mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-53880 Incorrect exhaustive pattern matching of Enum or Union types
Add PyNeverType and integrate it in PyTypeAssertionEvaluator GitOrigin-RevId: 3db5224bfbf559f6d1bb146fd72c6cc6a97c4598
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7216e0d719
commit
b54f5b4ae8
@@ -227,7 +227,7 @@ public final class PyUtilCore {
|
||||
return name == null ? 0 : name.startsWith("__") ? 2 : name.startsWith(PyNames.UNDERSCORE) ? 1 : 0;
|
||||
}
|
||||
|
||||
public static @Nullable List<@NotNull String> strListValue(PyAstExpression value) {
|
||||
public static @Nullable List<@NotNull String> strListValue(@Nullable PyAstExpression value) {
|
||||
while (value instanceof PyAstParenthesizedExpression) {
|
||||
value = ((PyAstParenthesizedExpression)value).getContainedExpression();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
package com.jetbrains.python.psi;
|
||||
|
||||
import com.jetbrains.python.ast.PyAstPattern;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface PyPattern extends PyAstPattern, PyTypedElement {
|
||||
/**
|
||||
* Returns the type that would be captured by this pattern when matching.
|
||||
* <p>
|
||||
* Unlike other PyTypedElements where getType returns their own type, pattern's getType
|
||||
* returns the type that would result from a successful match. For example:
|
||||
*
|
||||
* <pre>{@code
|
||||
* class Plant: pass
|
||||
* class Animal: pass
|
||||
* class Dog(Animal): pass
|
||||
*
|
||||
* x: Dog | Plant
|
||||
* match x:
|
||||
* case Animal():
|
||||
* # getType returns Dog here, even though the pattern is Animal()
|
||||
* }</pre>
|
||||
*
|
||||
* @see PyCapturePatternImpl#getCaptureType(PyPattern, TypeEvalContext)
|
||||
*/
|
||||
@Override
|
||||
@Nullable PyType getType(@NotNull TypeEvalContext context, TypeEvalContext.@NotNull Key key);
|
||||
|
||||
/**
|
||||
* Decides if the set of values described by a pattern is suitable
|
||||
* to be subtracted (excluded) from a subject type on the negative edge,
|
||||
* or if this pattern is too specific.
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
default boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -380,6 +380,7 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
|
||||
@Override
|
||||
public void visitWildcardPattern(@NotNull PyWildcardPattern node) {
|
||||
myBuilder.startNode(node);
|
||||
addTypeAssertionNodes(node, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -393,9 +394,9 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
|
||||
node.acceptChildren(this);
|
||||
myBuilder.updatePendingElementScope(node, node.getParent());
|
||||
|
||||
addTypeAssertionNodes(node, true);
|
||||
if (isRefutable) {
|
||||
myBuilder.addNode(new RefutablePatternInstruction(myBuilder, node, true));
|
||||
addTypeAssertionNodes(node, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,8 +1102,11 @@ public class PyControlFlowBuilder extends PyRecursiveElementVisitor {
|
||||
condition.accept(evaluator);
|
||||
for (PyTypeAssertionEvaluator.Assertion def : evaluator.getDefinitions()) {
|
||||
final PyQualifiedExpression e = def.getElement();
|
||||
final QualifiedName qname = e.asQualifiedName();
|
||||
final String name = qname != null ? qname.toString() : e.getName();
|
||||
String name = null;
|
||||
if (e != null) {
|
||||
final QualifiedName qname = e.asQualifiedName();
|
||||
name = qname != null ? qname.toString() : e.getName();
|
||||
}
|
||||
myBuilder.addNode(ReadWriteInstruction.assertType(myBuilder, e, name, def.getTypeEvalFunction()));
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -4,7 +4,9 @@ import com.intellij.codeInsight.controlflow.ControlFlow;
|
||||
import com.intellij.codeInsight.controlflow.ControlFlowUtil;
|
||||
import com.intellij.codeInsight.controlflow.Instruction;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
|
||||
import com.jetbrains.python.psi.types.PyNeverType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -12,7 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.*;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class PyDataFlow {
|
||||
public class PyDataFlow implements ControlFlow {
|
||||
private final TypeEvalContext myTypeEvalContext;
|
||||
private final Instruction[] myInstructions;
|
||||
private final boolean[] myReachability;
|
||||
@@ -41,7 +43,18 @@ public class PyDataFlow {
|
||||
private @NotNull Collection<Instruction> getReachableSuccessors(@NotNull Instruction instruction) {
|
||||
if (instruction instanceof CallInstruction ci && ci.isNoReturnCall(myTypeEvalContext)) return List.of();
|
||||
if (instruction instanceof PyWithContextExitInstruction wi && !wi.isSuppressingExceptions(myTypeEvalContext)) return List.of();
|
||||
return instruction.allSucc();
|
||||
return ContainerUtil.filter(instruction.allSucc(), next -> {
|
||||
if (next instanceof ReadWriteInstruction rw && rw.getAccess().isAssertTypeAccess()) {
|
||||
final var type = rw.getType(myTypeEvalContext, null);
|
||||
return !(type != null && type.get() instanceof PyNeverType);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instruction @NotNull [] getInstructions() {
|
||||
return myInstructions;
|
||||
}
|
||||
|
||||
public boolean isUnreachable(@NotNull Instruction instruction) {
|
||||
|
||||
+39
-14
@@ -183,7 +183,8 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
assert !myPositive; // for match statement as a whole, only negative assertion can be made
|
||||
final PyExpression subject = matchStatement.getSubject();
|
||||
if (subject == null) return;
|
||||
pushAssertion(subject, true, context -> {
|
||||
// allowAnyExpr is here because we need negative edges with Never even when subject is not reference expression
|
||||
pushAssertion(subject, true, true, context -> {
|
||||
PyType subjectType = context.getType(subject);
|
||||
for (PyCaseClause cs : matchStatement.getCaseClauses()) {
|
||||
if (cs.getPattern() == null) continue;
|
||||
@@ -211,14 +212,14 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
initialSubtype -> match(initialSubtype, suggestedSubtype, context)));
|
||||
|
||||
List<PyType> types = StreamEx.of(initialSubtypes).append(suggestedSubtypes).toList();
|
||||
return Ref.create(types.isEmpty() ? suggested : PyUnionType.union(types));
|
||||
return Ref.create(types.isEmpty() ? intersect(initial, suggested) : PyUnionType.union(types));
|
||||
}
|
||||
else {
|
||||
if (initial instanceof PyUnionType unionType) {
|
||||
return Ref.create(excludeFromUnion(unionType, suggested, context));
|
||||
}
|
||||
if (match(suggested, initial, context)) {
|
||||
return null;
|
||||
return Ref.create(PyNeverType.INSTANCE);
|
||||
}
|
||||
Ref<@Nullable PyType> diff = trySubtract(initial, suggested, context);
|
||||
return diff != null ? diff : Ref.create(initial);
|
||||
@@ -238,6 +239,9 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
members.add(m);
|
||||
}
|
||||
}
|
||||
if (members.isEmpty()) {
|
||||
return PyNeverType.INSTANCE;
|
||||
}
|
||||
return PyUnionType.union(members);
|
||||
}
|
||||
|
||||
@@ -256,11 +260,23 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
}
|
||||
List<PyLiteralType> enumMembers = PyStdlibTypeProvider.getEnumMembers(classType1.getPyClass(), context).toList();
|
||||
List<PyType> filteredEnumMembers = ContainerUtil.filter(enumMembers, m -> !PyTypeChecker.match(type2, m, context));
|
||||
if (filteredEnumMembers.isEmpty()) {
|
||||
return Ref.create(PyNeverType.INSTANCE);
|
||||
}
|
||||
PyType type = enumMembers.size() == filteredEnumMembers.size() ? type1 : PyUnionType.union(filteredEnumMembers);
|
||||
return Ref.create(type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable PyType intersect(@Nullable PyType initial, @Nullable PyType suggested) {
|
||||
// TODO: if we had IntersectionType, here it would be created. Also, final classes can be handled here
|
||||
if (initial instanceof PyNeverType) {
|
||||
return initial;
|
||||
}
|
||||
// While we don't have IntersectionType, return suggested
|
||||
return suggested;
|
||||
}
|
||||
|
||||
private static boolean match(@Nullable PyType expected, @Nullable PyType actual, @NotNull TypeEvalContext context) {
|
||||
return !(actual instanceof PyStructuralType) &&
|
||||
@@ -314,9 +330,16 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
}
|
||||
|
||||
private void pushAssertion(@Nullable PyExpression expr, boolean positive, @NotNull Function<TypeEvalContext, PyType> suggestedType) {
|
||||
pushAssertion(expr, positive, false, suggestedType);
|
||||
}
|
||||
|
||||
private void pushAssertion(@Nullable PyExpression expr, boolean positive, boolean allowAnyExpr, @NotNull Function<TypeEvalContext, PyType> suggestedType) {
|
||||
expr = PyPsiUtils.flattenParens(expr);
|
||||
if (expr instanceof PyReferenceExpression || expr instanceof PyTargetExpression) {
|
||||
final var target = (PyQualifiedExpression)expr;
|
||||
if (expr instanceof PyAssignmentExpression walrus) {
|
||||
pushAssertion(walrus.getTarget(), positive, allowAnyExpr, suggestedType);
|
||||
}
|
||||
else if (expr != null) {
|
||||
final var target = expr;
|
||||
final InstructionTypeCallback typeCallback = new InstructionTypeCallback() {
|
||||
@Override
|
||||
public Ref<PyType> getType(TypeEvalContext context) {
|
||||
@@ -324,10 +347,12 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
}
|
||||
};
|
||||
|
||||
myStack.push(new Assertion(target, typeCallback));
|
||||
}
|
||||
else if (expr instanceof PyAssignmentExpression walrus) {
|
||||
pushAssertion(walrus.getTarget(), positive, suggestedType);
|
||||
if (expr instanceof PyReferenceExpression || expr instanceof PyTargetExpression) {
|
||||
myStack.push(new Assertion((PyQualifiedExpression)target, typeCallback));
|
||||
}
|
||||
else if (allowAnyExpr) {
|
||||
myStack.push(new Assertion(null, typeCallback));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,19 +374,19 @@ public class PyTypeAssertionEvaluator extends PyRecursiveElementVisitor {
|
||||
}
|
||||
|
||||
static class Assertion {
|
||||
private final PyQualifiedExpression element;
|
||||
private final InstructionTypeCallback myFunction;
|
||||
private final @Nullable PyQualifiedExpression element;
|
||||
private final @NotNull InstructionTypeCallback myFunction;
|
||||
|
||||
Assertion(PyQualifiedExpression element, InstructionTypeCallback getType) {
|
||||
Assertion(@Nullable PyQualifiedExpression element, @NotNull InstructionTypeCallback getType) {
|
||||
this.element = element;
|
||||
this.myFunction = getType;
|
||||
}
|
||||
|
||||
public PyQualifiedExpression getElement() {
|
||||
public @Nullable PyQualifiedExpression getElement() {
|
||||
return element;
|
||||
}
|
||||
|
||||
public InstructionTypeCallback getTypeEvalFunction() {
|
||||
public @NotNull InstructionTypeCallback getTypeEvalFunction() {
|
||||
return myFunction;
|
||||
}
|
||||
}
|
||||
|
||||
+31
-30
@@ -23,6 +23,7 @@ import com.jetbrains.python.psi.PyElement;
|
||||
import com.jetbrains.python.psi.PyExpression;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -70,21 +71,21 @@ public final class ReadWriteInstruction extends InstructionImpl {
|
||||
}
|
||||
}
|
||||
|
||||
private final String myName;
|
||||
private final ACCESS myAccess;
|
||||
private final InstructionTypeCallback myGetType;
|
||||
private final @Nullable String myName;
|
||||
private final @NotNull ACCESS myAccess;
|
||||
private final @NotNull InstructionTypeCallback myGetType;
|
||||
|
||||
private ReadWriteInstruction(final ControlFlowBuilder builder,
|
||||
final PsiElement element,
|
||||
final String name,
|
||||
final ACCESS access) {
|
||||
private ReadWriteInstruction(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PsiElement element,
|
||||
final @Nullable String name,
|
||||
final @NotNull ACCESS access) {
|
||||
this(builder, element, name, access, null);
|
||||
}
|
||||
|
||||
private ReadWriteInstruction(final ControlFlowBuilder builder,
|
||||
final PsiElement element,
|
||||
final String name,
|
||||
final ACCESS access,
|
||||
private ReadWriteInstruction(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PsiElement element,
|
||||
final @Nullable String name,
|
||||
final @NotNull ACCESS access,
|
||||
final @Nullable InstructionTypeCallback getType) {
|
||||
super(builder, element);
|
||||
myName = name;
|
||||
@@ -92,37 +93,37 @@ public final class ReadWriteInstruction extends InstructionImpl {
|
||||
myGetType = getType != null ? getType : instructionTypeCallback(element);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
public @Nullable String getName() {
|
||||
return myName;
|
||||
}
|
||||
|
||||
public ACCESS getAccess() {
|
||||
public @NotNull ACCESS getAccess() {
|
||||
return myAccess;
|
||||
}
|
||||
|
||||
public static ReadWriteInstruction read(final ControlFlowBuilder builder,
|
||||
final PyElement element,
|
||||
final String name) {
|
||||
|
||||
public static @NotNull ReadWriteInstruction read(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PyElement element,
|
||||
final @Nullable String name) {
|
||||
return new ReadWriteInstruction(builder, element, name, ACCESS.READ);
|
||||
}
|
||||
|
||||
public static ReadWriteInstruction write(final ControlFlowBuilder builder,
|
||||
final PyElement element,
|
||||
final String name) {
|
||||
public static @NotNull ReadWriteInstruction write(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PyElement element,
|
||||
final @Nullable String name) {
|
||||
return new ReadWriteInstruction(builder, element, name, ACCESS.WRITE);
|
||||
}
|
||||
|
||||
public static ReadWriteInstruction newInstruction(final ControlFlowBuilder builder,
|
||||
final PsiElement element,
|
||||
final String name,
|
||||
final ACCESS access) {
|
||||
|
||||
public static @NotNull ReadWriteInstruction newInstruction(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PsiElement element,
|
||||
final @Nullable String name,
|
||||
final @NotNull ACCESS access) {
|
||||
return new ReadWriteInstruction(builder, element, name, access);
|
||||
}
|
||||
|
||||
public static ReadWriteInstruction assertType(final ControlFlowBuilder builder,
|
||||
final PsiElement element,
|
||||
final String name,
|
||||
final InstructionTypeCallback getType) {
|
||||
|
||||
public static @NotNull ReadWriteInstruction assertType(final @NotNull ControlFlowBuilder builder,
|
||||
final @Nullable PsiElement element,
|
||||
final @Nullable String name,
|
||||
final @Nullable InstructionTypeCallback getType) {
|
||||
return new ReadWriteInstruction(builder, element, name, ACCESS.ASSERTTYPE, getType);
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -868,6 +868,10 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
|
||||
return typeFromTypeAlias;
|
||||
}
|
||||
}
|
||||
final PyType neverType = getNeverType(resolved);
|
||||
if (neverType != null) {
|
||||
return Ref.create(neverType);
|
||||
}
|
||||
final PyType unionType = getUnionType(resolved, context);
|
||||
if (unionType != null) {
|
||||
return Ref.create(unionType);
|
||||
@@ -1458,6 +1462,15 @@ public final class PyTypingTypeProvider extends PyTypeProviderWithCustomContext<
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable PyType getNeverType(@NotNull PsiElement element) {
|
||||
var qName = getQualifiedName(element);
|
||||
if (qName == null) return null;
|
||||
if (List.of(NEVER, NEVER_EXT, NO_RETURN, NO_RETURN_EXT).contains(qName)) {
|
||||
return PyNeverType.INSTANCE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable PyType getUnionType(@NotNull PsiElement element, @NotNull Context context) {
|
||||
if (element instanceof PySubscriptionExpression subscriptionExpr) {
|
||||
final PyExpression operand = subscriptionExpr.getOperand();
|
||||
|
||||
@@ -6,11 +6,9 @@ import com.intellij.codeInsight.controlflow.ControlFlowUtil;
|
||||
import com.intellij.codeInsight.controlflow.Instruction;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.codeInsight.controlflow.CallInstruction;
|
||||
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
|
||||
import com.jetbrains.python.codeInsight.controlflow.PyWithContextExitInstruction;
|
||||
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
|
||||
import com.jetbrains.python.codeInsight.controlflow.*;
|
||||
import com.jetbrains.python.psi.PyStatementListContainer;
|
||||
import com.jetbrains.python.psi.types.PyNeverType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -23,8 +21,6 @@ public final class PyInspectionsUtil {
|
||||
|
||||
/**
|
||||
* Collects a list of unreachable elements, iterating through CFG backwards
|
||||
*
|
||||
* @param anchor the anchor element to start the iteration from, can be null to start from the last instruction
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static @NotNull List<PsiElement> collectUnreachable(@NotNull ScopeOwner owner, @Nullable PsiElement anchor, @NotNull TypeEvalContext context) {
|
||||
@@ -44,9 +40,14 @@ public final class PyInspectionsUtil {
|
||||
}
|
||||
|
||||
private static @NotNull List<Instruction> getReachablePredecessors(@NotNull Instruction instruction, @NotNull TypeEvalContext context) {
|
||||
// TODO: merge this with PyDataFlow
|
||||
return ContainerUtil.filter(instruction.allPred(), it -> {
|
||||
if (it instanceof CallInstruction ci && ci.isNoReturnCall(context)) return false;
|
||||
if (it instanceof PyWithContextExitInstruction wi && !wi.isSuppressingExceptions(context)) return false;
|
||||
if (it instanceof ReadWriteInstruction rw && rw.getAccess().isAssertTypeAccess()) {
|
||||
var type = rw.getType(context, null);
|
||||
return !(type != null && type.get() instanceof PyNeverType);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -55,6 +56,9 @@ public final class PyInspectionsUtil {
|
||||
if (instruction instanceof PyWithContextExitInstruction) {
|
||||
return null;
|
||||
}
|
||||
if (instruction instanceof ReadWriteInstruction rw && rw.getAccess().isAssertTypeAccess()) {
|
||||
return null;
|
||||
}
|
||||
PsiElement element = instruction.getElement();
|
||||
if (element instanceof PyStatementListContainer) {
|
||||
return ((PyStatementListContainer)element).getStatementList();
|
||||
|
||||
@@ -1119,7 +1119,7 @@ public final class PyUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @Nullable List<@NotNull String> strListValue(PyExpression value) {
|
||||
public static @Nullable List<@NotNull String> strListValue(@Nullable PyExpression value) {
|
||||
return PyUtilCore.strListValue(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,9 @@ public class PyAsPatternImpl extends PyElementImpl implements PyAsPattern {
|
||||
public @Nullable PyType getType(@NotNull TypeEvalContext context, TypeEvalContext.@NotNull Key key) {
|
||||
return context.getType(getPattern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
return getPattern().canExcludePatternType(context);
|
||||
}
|
||||
}
|
||||
|
||||
+84
-50
@@ -5,7 +5,6 @@ import com.intellij.openapi.util.Ref;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.jetbrains.python.codeInsight.controlflow.PyTypeAssertionEvaluator;
|
||||
import com.jetbrains.python.codeInsight.stdlib.PyDataclassTypeProvider;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.resolve.PyResolveContext;
|
||||
@@ -17,6 +16,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.controlflow.PyTypeAssertionEvaluator.createAssertionType;
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
import static com.jetbrains.python.psi.impl.PySequencePatternImpl.wrapInListType;
|
||||
|
||||
@@ -39,8 +39,28 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
"bool", "bytearray", "bytes", "dict", "float", "frozenset", "int", "list", "set", "str", "tuple");
|
||||
|
||||
/**
|
||||
* Determines the type of a given pattern assuming it is a capture pattern (even when it is actually not),
|
||||
* and looking up (to parents or subject expression of the match statement).
|
||||
* Determines what type this pattern would have if it was a capture pattern (like a bare name or _).
|
||||
* <p>
|
||||
* In pattern matching, a capture pattern takes on the type of the entire matched expression,
|
||||
* regardless of any specific pattern constraints.
|
||||
* <p>
|
||||
* For example:
|
||||
* <pre>{@code
|
||||
* x: int | str
|
||||
* match x:
|
||||
* case a: # This is a capture pattern
|
||||
* # Here 'a' has type int | str
|
||||
* case str(): # This is a class pattern
|
||||
* # Capture type: int | str (same as what 'case a:' would get)
|
||||
* # Regular getType: str
|
||||
*
|
||||
* y: int
|
||||
* match y:
|
||||
* case str() as a:
|
||||
* # Capture type: int (same as what 'case a:' would get)
|
||||
* # Regular getType: intersect(int, str) (just 'str' for now)
|
||||
* }</pre>
|
||||
* @see PyPattern#getType(TypeEvalContext, TypeEvalContext.Key)
|
||||
*/
|
||||
static @Nullable PyType getCaptureType(@NotNull PyPattern pattern, @NotNull TypeEvalContext context) {
|
||||
final PyElement parentPattern = PsiTreeUtil.getParentOfType(
|
||||
@@ -48,7 +68,7 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
PyCaseClause.class, // - Subject of a match statement
|
||||
PySingleStarPattern.class, // - Type of parent sequence pattern
|
||||
PyDoubleStarPattern.class, // - Type of parent mapping pattern
|
||||
PyKeyValuePattern.class, // - Any
|
||||
PyKeyValuePattern.class, // - Value type of parent mapping pattern
|
||||
PySequencePattern.class, // - Iterated item type of sequence
|
||||
PyClassPattern.class, // - Attribute type in the corresponding class
|
||||
PyKeywordPattern.class // - Attribute type in the corresponding class
|
||||
@@ -65,9 +85,10 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
for (PyCaseClause cs : matchStatement.getCaseClauses()) {
|
||||
if (cs == caseClause) break;
|
||||
if (cs.getPattern() == null) continue;
|
||||
if (cs.getGuardCondition() != null) continue;
|
||||
subjectType = Ref.deref(
|
||||
PyTypeAssertionEvaluator.createAssertionType(subjectType, context.getType(cs.getPattern()), false, context));
|
||||
if (cs.getGuardCondition() != null && !PyEvaluator.evaluateAsBoolean(cs.getGuardCondition(), false)) continue;
|
||||
if (cs.getPattern().canExcludePatternType(context)) {
|
||||
subjectType = Ref.deref(createAssertionType(subjectType, context.getType(cs.getPattern()), false, context));
|
||||
}
|
||||
}
|
||||
|
||||
return subjectType;
|
||||
@@ -83,8 +104,8 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
if (parentPattern instanceof PyDoubleStarPattern) {
|
||||
final PyMappingPattern mappingParent = as(parentPattern.getParent(), PyMappingPattern.class);
|
||||
if (mappingParent == null) return null;
|
||||
var parentType = context.getType(mappingParent);
|
||||
if (parentType instanceof PyCollectionType collectionType) {
|
||||
var mappingType = PyTypeUtil.convertToType(context.getType(mappingParent), "typing.Mapping", pattern, context);
|
||||
if (mappingType instanceof PyCollectionType collectionType) {
|
||||
final PyClass dict = PyBuiltinCache.getInstance(pattern).getClass("dict");
|
||||
return dict != null ? new PyCollectionTypeImpl(dict, false, collectionType.getElementTypes()) : null;
|
||||
}
|
||||
@@ -94,30 +115,36 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
final PyMappingPattern mappingParent = as(keyValuePattern.getParent(), PyMappingPattern.class);
|
||||
if (mappingParent == null) return null;
|
||||
|
||||
var dictType = getCaptureType(mappingParent, context);
|
||||
if (dictType == null) return null;
|
||||
|
||||
if (dictType instanceof PyTypedDictType typedDictType) {
|
||||
if (context.getType(keyValuePattern.getKeyPattern()) instanceof PyLiteralType l && l.getExpression() instanceof PyStringLiteralExpression str) {
|
||||
return typedDictType.getElementType(str.getStringValue());
|
||||
return PyTypeUtil.toStream(getCaptureType(mappingParent, context)).map(type -> {
|
||||
if (type instanceof PyTypedDictType typedDictType) {
|
||||
if (context.getType(keyValuePattern.getKeyPattern()) instanceof PyLiteralType l &&
|
||||
l.getExpression() instanceof PyStringLiteralExpression str) {
|
||||
return typedDictType.getElementType(str.getStringValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
var mappingType = PyTypeUtil.convertToType(dictType, "typing.Mapping", pattern, context);
|
||||
if (mappingType instanceof PyCollectionType collectionType) {
|
||||
return collectionType.getElementTypes().get(1);
|
||||
}
|
||||
return null;
|
||||
|
||||
PyType mappingType = PyTypeUtil.convertToType(type, "typing.Mapping", pattern, context);
|
||||
if (mappingType == null) {
|
||||
return PyNeverType.INSTANCE;
|
||||
}
|
||||
else if (mappingType instanceof PyCollectionType collectionType) {
|
||||
return collectionType.getElementTypes().get(1);
|
||||
}
|
||||
return null;
|
||||
}).collect(PyTypeUtil.toUnion());
|
||||
}
|
||||
if (parentPattern instanceof PySequencePattern sequencePattern) {
|
||||
final PyType sequenceType = PySequencePatternImpl.getSequenceCaptureType(sequencePattern, context);
|
||||
if (sequenceType == null) return null;
|
||||
|
||||
// This is done to skip group- and as-patterns
|
||||
final var sequenceMember = PsiTreeUtil.findFirstParent(pattern, el -> el.getParent() == sequencePattern);
|
||||
final List<PyPattern> elements = sequencePattern.getElements();
|
||||
final int idx = elements.indexOf(sequenceMember);
|
||||
final int starIdx = ContainerUtil.indexOf(elements, it2 -> it2 instanceof PySingleStarPattern);
|
||||
|
||||
return PyTypeUtil.toStream(sequenceType).map(it -> {
|
||||
if (it instanceof PyTupleType tupleType && !tupleType.isHomogeneous()) {
|
||||
// This is done to skip group- and as-patterns
|
||||
final var sequenceMember = PsiTreeUtil.findFirstParent(pattern, el -> el.getParent() == sequencePattern);
|
||||
final List<PyPattern> elements = sequencePattern.getElements();
|
||||
final int idx = elements.indexOf(sequenceMember);
|
||||
final int starIdx = ContainerUtil.indexOf(elements, it2 -> it2 instanceof PySingleStarPattern);
|
||||
if (starIdx == -1 || idx < starIdx) {
|
||||
return tupleType.getElementType(idx);
|
||||
}
|
||||
@@ -134,32 +161,30 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
}).collect(PyTypeUtil.toUnion());
|
||||
}
|
||||
if (parentPattern instanceof PyClassPattern classPattern) {
|
||||
if (context.getType(classPattern) instanceof PyClassType classType) {
|
||||
final List<PyPattern> arguments = classPattern.getArgumentList().getPatterns();
|
||||
int index = arguments.indexOf(pattern);
|
||||
if (index < 0) return null;
|
||||
|
||||
if (SPECIAL_BUILTINS.contains(classType.getClassQName())) {
|
||||
if (index == 0) {
|
||||
var classCapture = PyTypeUtil.toStream(getCaptureType(classPattern, context))
|
||||
.filter(it -> PyTypeChecker.match(classType, it, context))
|
||||
.collect(PyTypeUtil.toUnion());
|
||||
return classCapture != null ? classCapture : classType;
|
||||
final List<PyPattern> arguments = classPattern.getArgumentList().getPatterns();
|
||||
int index = arguments.indexOf(pattern);
|
||||
if (index < 0) return null;
|
||||
|
||||
// capture type can be a union like: list[int] | list[str]
|
||||
return PyTypeUtil.toStream(context.getType(classPattern)).map(type -> {
|
||||
if (type instanceof PyClassType classType) {
|
||||
if (SPECIAL_BUILTINS.contains(classType.getClassQName())) {
|
||||
if (index == 0) {
|
||||
return classType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
final PyClass cls = classType.getPyClass();
|
||||
List<String> matchArgs = cls.getOwnMatchArgs();
|
||||
if (matchArgs == null) {
|
||||
matchArgs = PyDataclassTypeProvider.Companion.getGeneratedMatchArgs(cls, context);
|
||||
}
|
||||
if (matchArgs == null || matchArgs.size() > arguments.size()) return null;
|
||||
List<String> matchArgs = getMatchArgs(classType, context);
|
||||
if (matchArgs == null || matchArgs.size() > arguments.size()) return null;
|
||||
|
||||
final PyTypedElement instanceAttribute = as(resolveTypeMember(classType, matchArgs.get(index), context), PyTypedElement.class);
|
||||
return instanceAttribute != null ? context.getType(instanceAttribute) : null;
|
||||
}
|
||||
return null;
|
||||
final PyTypedElement instanceAttribute = as(resolveTypeMember(classType, matchArgs.get(index), context), PyTypedElement.class);
|
||||
if (instanceAttribute == null) return null;
|
||||
|
||||
return PyTypeChecker.substitute(context.getType(instanceAttribute), PyTypeChecker.unifyReceiver(classType, context), context);
|
||||
}
|
||||
return null;
|
||||
}).collect(PyTypeUtil.toUnion());
|
||||
}
|
||||
if (parentPattern instanceof PyKeywordPattern keywordPattern) {
|
||||
final PyClassPattern classPattern = PsiTreeUtil.getParentOfType(keywordPattern, PyClassPattern.class);
|
||||
@@ -174,9 +199,18 @@ public class PyCapturePatternImpl extends PyElementImpl implements PyCapturePatt
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static @Nullable List<@NotNull String> getMatchArgs(@NotNull PyClassType type, @NotNull TypeEvalContext context) {
|
||||
final PyClass cls = type.getPyClass();
|
||||
List<String> matchArgs = cls.getOwnMatchArgs();
|
||||
if (matchArgs == null) {
|
||||
matchArgs = PyDataclassTypeProvider.Companion.getGeneratedMatchArgs(cls, context);
|
||||
}
|
||||
return matchArgs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PsiElement resolveTypeMember(@NotNull PyType type, @NotNull String name, @NotNull TypeEvalContext context) {
|
||||
static PsiElement resolveTypeMember(@NotNull PyType type, @NotNull String name, @NotNull TypeEvalContext context) {
|
||||
final PyResolveContext resolveContext = PyResolveContext.defaultContext(context);
|
||||
final List<? extends RatedResolveResult> results = type.resolveMember(name, null, AccessDirection.READ, resolveContext);
|
||||
return !ContainerUtil.isEmpty(results) ? results.get(0).getElement() : null;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package com.jetbrains.python.psi.impl;
|
||||
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.jetbrains.python.psi.PyClassPattern;
|
||||
import com.jetbrains.python.psi.PyElementVisitor;
|
||||
import com.jetbrains.python.psi.types.PyClassType;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.PyTypeChecker;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import com.intellij.openapi.util.Ref;
|
||||
import com.jetbrains.python.codeInsight.controlflow.PyTypeAssertionEvaluator;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.types.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.intellij.util.containers.ContainerUtil.getFirstItem;
|
||||
import static com.jetbrains.python.psi.PyUtil.multiResolveTopPriority;
|
||||
import static com.jetbrains.python.psi.impl.PyCapturePatternImpl.*;
|
||||
|
||||
public class PyClassPatternImpl extends PyElementImpl implements PyClassPattern {
|
||||
public PyClassPatternImpl(ASTNode astNode) {
|
||||
super(astNode);
|
||||
@@ -26,10 +30,64 @@ public class PyClassPatternImpl extends PyElementImpl implements PyClassPattern
|
||||
if (type instanceof PyClassType classType) {
|
||||
final PyType instanceType = classType.toInstance();
|
||||
final PyType captureType = PyCapturePatternImpl.getCaptureType(this, context);
|
||||
if (PyTypeChecker.match(captureType, instanceType, context)) {
|
||||
return instanceType;
|
||||
}
|
||||
return Ref.deref(PyTypeAssertionEvaluator.createAssertionType(captureType, instanceType, true, context));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
final var refName = getClassNameReference();
|
||||
final var resolved = getFirstItem(multiResolveTopPriority(refName.getReference()));
|
||||
if (!(resolved instanceof PyClass)) {
|
||||
// def foo(clz: type[Clz], x):
|
||||
// match x:
|
||||
// case clz(): # <-- can actually be a subclass of Clz, so doesn't subtract
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(context.getType(refName) instanceof PyClassType classType)) return false;
|
||||
|
||||
final List<PyPattern> arguments = getArgumentList().getPatterns();
|
||||
if (SPECIAL_BUILTINS.contains(classType.getClassQName())) {
|
||||
if (arguments.isEmpty()) return true;
|
||||
if (arguments.size() > 1) return false;
|
||||
return arguments.get(0).canExcludePatternType(context);
|
||||
}
|
||||
|
||||
List<String> matchArgs = getMatchArgs(classType, context);
|
||||
for (int i = 0; i < arguments.size(); i++) {
|
||||
final var member = arguments.get(i);
|
||||
if (member instanceof PyKeywordPattern keywordPattern) {
|
||||
if (resolveTypeMember(classType, keywordPattern.getKeyword(), context) == null) {
|
||||
return false;
|
||||
}
|
||||
final var valuePattern = keywordPattern.getValuePattern();
|
||||
if (valuePattern != null && !canExcludeArgumentPatternType(valuePattern, context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (matchArgs == null) return false;
|
||||
if (i >= matchArgs.size()) return false;
|
||||
if (!canExcludeArgumentPatternType(member, context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
final PyType captureType = getCaptureType(this, context);
|
||||
final PyType patternType = context.getType(this);
|
||||
return PyTypeUtil.toStream(captureType).anyMatch(it -> PyTypeChecker.match(patternType, it, context));
|
||||
}
|
||||
|
||||
static boolean canExcludeArgumentPatternType(@NotNull PyPattern pattern, @NotNull TypeEvalContext context) {
|
||||
final var captureType = getCaptureType(pattern, context);
|
||||
final var patternType = context.getType(pattern);
|
||||
// For class pattern arguments, we need to ensure that the argument pattern covers its capture type fully
|
||||
if (Ref.deref(PyTypeAssertionEvaluator.createAssertionType(captureType, patternType, false, context)) instanceof PyNeverType) {
|
||||
// in case the argument pattern is also class pattern with arguments
|
||||
return pattern.canExcludePatternType(context);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,9 @@ public class PyGroupPatternImpl extends PyElementImpl implements PyGroupPattern
|
||||
public @Nullable PyType getType(@NotNull TypeEvalContext context, TypeEvalContext.@NotNull Key key) {
|
||||
return context.getType(getPattern());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
return getPattern().canExcludePatternType(context);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -3,14 +3,10 @@ package com.jetbrains.python.psi.impl;
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.jetbrains.python.psi.PyElementVisitor;
|
||||
import com.jetbrains.python.psi.PyKeyValuePattern;
|
||||
import com.jetbrains.python.psi.PyPattern;
|
||||
import com.jetbrains.python.psi.types.PyTupleType;
|
||||
import com.jetbrains.python.psi.types.PyType;
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PyKeyValuePatternImpl extends PyElementImpl implements PyKeyValuePattern {
|
||||
public PyKeyValuePatternImpl(ASTNode astNode) {
|
||||
super(astNode);
|
||||
@@ -23,12 +19,6 @@ public class PyKeyValuePatternImpl extends PyElementImpl implements PyKeyValuePa
|
||||
|
||||
@Override
|
||||
public PyType getType(@NotNull TypeEvalContext context, @NotNull TypeEvalContext.Key key) {
|
||||
final PyType keyType = context.getType(getKeyPattern());
|
||||
final PyPattern value = getValuePattern();
|
||||
PyType valueType = null;
|
||||
if (value != null) {
|
||||
valueType = context.getType(value);
|
||||
}
|
||||
return PyTupleType.create(this, Arrays.asList(keyType, valueType));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,20 +27,33 @@ public class PyMappingPatternImpl extends PyElementImpl implements PyMappingPatt
|
||||
return Arrays.asList(findChildrenByClass(PyKeyValuePattern.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PyType getType(@NotNull TypeEvalContext context, TypeEvalContext.@NotNull Key key) {
|
||||
ArrayList<PyType> keyTypes = new ArrayList<>();
|
||||
ArrayList<PyType> valueTypes = new ArrayList<>();
|
||||
for (PyKeyValuePattern it : getComponents()) {
|
||||
PyType type = context.getType(it);
|
||||
if (type instanceof PyTupleType tupleType) {
|
||||
keyTypes.add(tupleType.getElementType(0));
|
||||
valueTypes.add(tupleType.getElementType(1));
|
||||
keyTypes.add(context.getType(it.getKeyPattern()));
|
||||
if (it.getValuePattern() != null) {
|
||||
valueTypes.add(context.getType(it.getValuePattern()));
|
||||
}
|
||||
}
|
||||
//keyTypes.add(null);
|
||||
//valueTypes.add(null);
|
||||
return wrapInMappingType(PyUnionType.union(keyTypes), PyUnionType.union(valueTypes), this);
|
||||
|
||||
PyType patternMappingType = wrapInMappingType(PyUnionType.union(keyTypes), PyUnionType.union(valueTypes), this);
|
||||
|
||||
PyType captureTypes = PyCapturePatternImpl.getCaptureType(this, context);
|
||||
PyType filteredType = PyTypeUtil.toStream(captureTypes).filter(captureType -> {
|
||||
var mappingType = PyTypeUtil.convertToType(captureType, "typing.Mapping", this, context);
|
||||
if (mappingType == null) return false;
|
||||
if (!PyTypeChecker.match(mappingType, patternMappingType, context)) return false;
|
||||
return true;
|
||||
}).collect(PyTypeUtil.toUnion());
|
||||
|
||||
return filteredType == null ? patternMappingType : filteredType;
|
||||
}
|
||||
|
||||
private static @Nullable PyType wrapInMappingType(@Nullable PyType keyType, @Nullable PyType valueType, @NotNull PsiElement resolveAnchor) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.jetbrains.python.psi.impl.PyClassPatternImpl.canExcludeArgumentPatternType;
|
||||
import static com.jetbrains.python.psi.types.PyLiteralType.upcastLiteralToClass;
|
||||
|
||||
public class PySequencePatternImpl extends PyElementImpl implements PySequencePattern, PsiListLikeElement {
|
||||
@@ -55,6 +56,23 @@ public class PySequencePatternImpl extends PyElementImpl implements PySequencePa
|
||||
})
|
||||
.collect(PyTypeUtil.toUnion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExcludePatternType(@NotNull TypeEvalContext context) {
|
||||
for (var p : getElements()) {
|
||||
if (!canExcludeArgumentPatternType(p, context)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (var type : PyTypeUtil.toStream(getSequenceCaptureType(this, context))) {
|
||||
if (!(type instanceof PyTupleType tupleType) || tupleType.isHomogeneous()) {
|
||||
// Not clear how to do this accurately, so for now matching
|
||||
// types like list[Something] will not exclude type in any case
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static @Nullable PyType wrapInListType(@Nullable PyType elementType, @NotNull PsiElement resolveAnchor) {
|
||||
final PyClass list = PyBuiltinCache.getInstance(resolveAnchor).getClass("list");
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.jetbrains.python.psi.types
|
||||
|
||||
object PyNeverType: PyUnionType(LinkedHashSet()) {
|
||||
override fun getName(): String = "Never"
|
||||
}
|
||||
@@ -113,6 +113,14 @@ public final class PyTypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
if (actual instanceof PyNeverType) {
|
||||
return Optional.of(true);
|
||||
}
|
||||
|
||||
if (expected instanceof PyNeverType) {
|
||||
return Optional.of(false);
|
||||
}
|
||||
|
||||
if (expected instanceof PyClassType) {
|
||||
Optional<Boolean> match = matchObject((PyClassType)expected, actual);
|
||||
if (match.isPresent()) {
|
||||
@@ -1446,7 +1454,7 @@ public final class PyTypeChecker {
|
||||
return new GenericSubstitutions();
|
||||
}
|
||||
|
||||
static @NotNull GenericSubstitutions unifyReceiver(@Nullable PyType receiverType, @NotNull TypeEvalContext context) {
|
||||
public static @NotNull GenericSubstitutions unifyReceiver(@Nullable PyType receiverType, @NotNull TypeEvalContext context) {
|
||||
// Collect generic params of object type
|
||||
final var substitutions = new GenericSubstitutions();
|
||||
if (receiverType != null) {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
6(7) element: PyMatchStatement
|
||||
7(8) READ ACCESS: x
|
||||
8(9,12) refutable pattern: 42
|
||||
9(10) matched pattern: 42
|
||||
10(11) ASSERTTYPE ACCESS: x
|
||||
9(10) ASSERTTYPE ACCESS: x
|
||||
10(11) matched pattern: 42
|
||||
11(15) element: PyBreakStatement
|
||||
12(13) ASSERTTYPE ACCESS: x
|
||||
13(14) element: PyExpressionStatement
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
6(7) element: PyMatchStatement
|
||||
7(8) READ ACCESS: x
|
||||
8(9,12) refutable pattern: 42
|
||||
9(10) matched pattern: 42
|
||||
10(11) ASSERTTYPE ACCESS: x
|
||||
9(10) ASSERTTYPE ACCESS: x
|
||||
10(11) matched pattern: 42
|
||||
11(1) element: PyContinueStatement
|
||||
12(13) ASSERTTYPE ACCESS: x
|
||||
13(14) element: PyExpressionStatement
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
2(3) element: PyMatchStatement
|
||||
3(4) READ ACCESS: x
|
||||
4(5,8) refutable pattern: 42
|
||||
5(6) matched pattern: 42
|
||||
6(7) ASSERTTYPE ACCESS: x
|
||||
5(6) ASSERTTYPE ACCESS: x
|
||||
6(7) matched pattern: 42
|
||||
7(11) element: PyReturnStatement
|
||||
8(9) ASSERTTYPE ACCESS: x
|
||||
9(10) element: PyExpressionStatement
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
0(1) element: null
|
||||
1(2) element: PyMatchStatement
|
||||
2(3,11) refutable pattern: 1
|
||||
2(3,12) refutable pattern: 1
|
||||
3(4) matched pattern: 1
|
||||
4(5) element: PyMatchStatement
|
||||
5(6,9) refutable pattern: 11
|
||||
6(7) matched pattern: 11
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: y11
|
||||
9(10) element: PyExpressionStatement
|
||||
10(15) READ ACCESS: y1
|
||||
11(12,15) refutable pattern: 2
|
||||
12(13) matched pattern: 2
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: y2
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: z
|
||||
17() element: null
|
||||
8(10) READ ACCESS: y11
|
||||
9(10) ASSERTTYPE ACCESS: null
|
||||
10(11) element: PyExpressionStatement
|
||||
11(17) READ ACCESS: y1
|
||||
12(13,16) refutable pattern: 2
|
||||
13(14) matched pattern: 2
|
||||
14(15) element: PyExpressionStatement
|
||||
15(17) READ ACCESS: y2
|
||||
16(17) ASSERTTYPE ACCESS: null
|
||||
17(18) element: PyExpressionStatement
|
||||
18(19) READ ACCESS: z
|
||||
19() element: null
|
||||
+12
-10
@@ -1,16 +1,18 @@
|
||||
0(1) element: null
|
||||
1(2) element: PyMatchStatement
|
||||
2(3,9) refutable pattern: 1
|
||||
2(3,10) refutable pattern: 1
|
||||
3(4) matched pattern: 1
|
||||
4(5) element: PyMatchStatement
|
||||
5(6,13) refutable pattern: 11
|
||||
5(6,9) refutable pattern: 11
|
||||
6(7) matched pattern: 11
|
||||
7(8) element: PyExpressionStatement
|
||||
8(13) READ ACCESS: y11
|
||||
9(10,13) refutable pattern: 2
|
||||
10(11) matched pattern: 2
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: y2
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: z
|
||||
15() element: null
|
||||
8(15) READ ACCESS: y11
|
||||
9(15) ASSERTTYPE ACCESS: null
|
||||
10(11,14) refutable pattern: 2
|
||||
11(12) matched pattern: 2
|
||||
12(13) element: PyExpressionStatement
|
||||
13(15) READ ACCESS: y2
|
||||
14(15) ASSERTTYPE ACCESS: null
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: z
|
||||
17() element: null
|
||||
+5
-4
@@ -12,7 +12,8 @@
|
||||
11(12) matched pattern: [42] | foo.bar
|
||||
12(13) WRITE ACCESS: x
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: y
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: z
|
||||
17() element: null
|
||||
14(16) READ ACCESS: y
|
||||
15(16) ASSERTTYPE ACCESS: null
|
||||
16(17) element: PyExpressionStatement
|
||||
17(18) READ ACCESS: z
|
||||
18() element: null
|
||||
+5
-4
@@ -4,7 +4,8 @@
|
||||
3(4) WRITE ACCESS: x
|
||||
4(5) matched pattern: [x]
|
||||
5(6) element: PyExpressionStatement
|
||||
6(7) READ ACCESS: y
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: z
|
||||
9() element: null
|
||||
6(8) READ ACCESS: y
|
||||
7(8) ASSERTTYPE ACCESS: null
|
||||
8(9) element: PyExpressionStatement
|
||||
9(10) READ ACCESS: z
|
||||
10() element: null
|
||||
@@ -11,7 +11,8 @@
|
||||
10(11) matched pattern: attr=foo.bar
|
||||
11(12) matched pattern: Class(1, attr=foo.bar)
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: x
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: y
|
||||
16() element: null
|
||||
13(15) READ ACCESS: x
|
||||
14(15) ASSERTTYPE ACCESS: null
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: y
|
||||
17() element: null
|
||||
+5
-4
@@ -9,7 +9,8 @@
|
||||
8(12) element: null. Condition: x < 10:false
|
||||
9(10) element: null. Condition: x < 10:true
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: y
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: z
|
||||
14() element: null
|
||||
11(13) READ ACCESS: y
|
||||
12(13) ASSERTTYPE ACCESS: null
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: z
|
||||
15() element: null
|
||||
+5
-4
@@ -13,7 +13,8 @@
|
||||
12(16) element: null. Condition: x % 100 != 0:false
|
||||
13(14) element: null. Condition: x % 100 != 0:true
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: y
|
||||
16(17) element: PyExpressionStatement
|
||||
17(18) READ ACCESS: z
|
||||
18() element: null
|
||||
15(17) READ ACCESS: y
|
||||
16(17) ASSERTTYPE ACCESS: null
|
||||
17(18) element: PyExpressionStatement
|
||||
18(19) READ ACCESS: z
|
||||
19() element: null
|
||||
+5
-4
@@ -9,7 +9,8 @@
|
||||
8(12) element: null. Condition: x < 0:false
|
||||
9(10) element: null. Condition: x < 0:true
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: y
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: z
|
||||
14() element: null
|
||||
11(13) READ ACCESS: y
|
||||
12(13) ASSERTTYPE ACCESS: null
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: z
|
||||
15() element: null
|
||||
+5
-4
@@ -10,7 +10,8 @@
|
||||
9(10) WRITE ACCESS: x
|
||||
10(11) matched pattern: {"foo": 1, **x}
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: y
|
||||
13(14) element: PyExpressionStatement
|
||||
14(15) READ ACCESS: z
|
||||
15() element: null
|
||||
12(14) READ ACCESS: y
|
||||
13(14) ASSERTTYPE ACCESS: null
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: z
|
||||
16() element: null
|
||||
+5
-4
@@ -16,7 +16,8 @@
|
||||
15(19) element: null. Condition: (x1 or x2) > x3:false
|
||||
16(17) element: null. Condition: (x1 or x2) > x3:true
|
||||
17(18) element: PyExpressionStatement
|
||||
18(19) READ ACCESS: y
|
||||
19(20) element: PyExpressionStatement
|
||||
20(21) READ ACCESS: z
|
||||
21() element: null
|
||||
18(20) READ ACCESS: y
|
||||
19(20) ASSERTTYPE ACCESS: null
|
||||
20(21) element: PyExpressionStatement
|
||||
21(22) READ ACCESS: z
|
||||
22() element: null
|
||||
+5
-4
@@ -7,7 +7,8 @@
|
||||
6(7) matched pattern: [x]
|
||||
7(8) matched pattern: x | [x]
|
||||
8(9) element: PyExpressionStatement
|
||||
9(10) READ ACCESS: y
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
9(11) READ ACCESS: y
|
||||
10(11) ASSERTTYPE ACCESS: null
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: z
|
||||
13() element: null
|
||||
+5
-4
@@ -3,7 +3,8 @@
|
||||
2(3,6) refutable pattern: 42
|
||||
3(4) matched pattern: 42
|
||||
4(5) element: PyExpressionStatement
|
||||
5(6) READ ACCESS: y
|
||||
6(7) element: PyExpressionStatement
|
||||
7(8) READ ACCESS: z
|
||||
8() element: null
|
||||
5(7) READ ACCESS: y
|
||||
6(7) ASSERTTYPE ACCESS: null
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: z
|
||||
9() element: null
|
||||
+5
-4
@@ -16,7 +16,8 @@
|
||||
15(16) matched pattern: 'bar': foo.bar
|
||||
16(17) matched pattern: {'foo': 1, 'bar': foo.bar}
|
||||
17(18) element: PyExpressionStatement
|
||||
18(19) READ ACCESS: x
|
||||
19(20) element: PyExpressionStatement
|
||||
20(21) READ ACCESS: y
|
||||
21() element: null
|
||||
18(20) READ ACCESS: x
|
||||
19(20) ASSERTTYPE ACCESS: null
|
||||
20(21) element: PyExpressionStatement
|
||||
21(22) READ ACCESS: y
|
||||
22() element: null
|
||||
+5
-4
@@ -6,7 +6,8 @@
|
||||
5(6) WRITE ACCESS: x
|
||||
6(7) matched pattern: [1, *x]
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: y
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: z
|
||||
11() element: null
|
||||
8(10) READ ACCESS: y
|
||||
9(10) ASSERTTYPE ACCESS: null
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
+5
-4
@@ -11,7 +11,8 @@
|
||||
10(11) matched pattern: 2 | 3
|
||||
11(12) matched pattern: 1 | (2 | 3)
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: x
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: y
|
||||
16() element: null
|
||||
13(15) READ ACCESS: x
|
||||
14(15) ASSERTTYPE ACCESS: null
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: y
|
||||
17() element: null
|
||||
+5
-4
@@ -11,7 +11,8 @@
|
||||
10(11) WRITE ACCESS: x
|
||||
11(12) matched pattern: [x] | (foo.bar as x)
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: y
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: z
|
||||
16() element: null
|
||||
13(15) READ ACCESS: y
|
||||
14(15) ASSERTTYPE ACCESS: null
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: z
|
||||
17() element: null
|
||||
+5
-4
@@ -7,7 +7,8 @@
|
||||
6(7) matched pattern: 42
|
||||
7(8) matched pattern: [] | 42
|
||||
8(9) element: PyExpressionStatement
|
||||
9(10) READ ACCESS: y
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
9(11) READ ACCESS: y
|
||||
10(11) ASSERTTYPE ACCESS: null
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: z
|
||||
13() element: null
|
||||
+5
-4
@@ -6,7 +6,8 @@
|
||||
5(6) matched pattern: 42
|
||||
6(7) matched pattern: _ | 42
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: y
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: z
|
||||
11() element: null
|
||||
8(10) READ ACCESS: y
|
||||
9(10) ASSERTTYPE ACCESS: null
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
+5
-4
@@ -11,7 +11,8 @@
|
||||
10(14) element: null. Condition: x % 2 == 0:false
|
||||
11(12) element: null. Condition: x % 2 == 0:true
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: y
|
||||
14(15) element: PyExpressionStatement
|
||||
15(16) READ ACCESS: z
|
||||
16() element: null
|
||||
13(15) READ ACCESS: y
|
||||
14(15) ASSERTTYPE ACCESS: null
|
||||
15(16) element: PyExpressionStatement
|
||||
16(17) READ ACCESS: z
|
||||
17() element: null
|
||||
+5
-4
@@ -8,7 +8,8 @@
|
||||
7(8) matched pattern: foo.bar
|
||||
8(9) matched pattern: [1, foo.bar]
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: x
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: y
|
||||
13() element: null
|
||||
10(12) READ ACCESS: x
|
||||
11(12) ASSERTTYPE ACCESS: null
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: y
|
||||
14() element: null
|
||||
+5
-4
@@ -8,7 +8,8 @@
|
||||
7(8) matched pattern: 1 | x
|
||||
8(9) matched pattern: [1 | x]
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: y
|
||||
11(12) element: PyExpressionStatement
|
||||
12(13) READ ACCESS: z
|
||||
13() element: null
|
||||
10(12) READ ACCESS: y
|
||||
11(12) ASSERTTYPE ACCESS: null
|
||||
12(13) element: PyExpressionStatement
|
||||
13(14) READ ACCESS: z
|
||||
14() element: null
|
||||
@@ -5,7 +5,8 @@
|
||||
4(8) element: null. Condition: x > 0:false
|
||||
5(6) element: null. Condition: x > 0:true
|
||||
6(7) element: PyExpressionStatement
|
||||
7(8) READ ACCESS: y
|
||||
8(9) element: PyExpressionStatement
|
||||
9(10) READ ACCESS: z
|
||||
10() element: null
|
||||
7(9) READ ACCESS: y
|
||||
8(9) ASSERTTYPE ACCESS: null
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: z
|
||||
11() element: null
|
||||
+5
-4
@@ -6,7 +6,8 @@
|
||||
5(6) element: PyWildcardPattern
|
||||
6(7) matched pattern: [1, *_]
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: y
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: z
|
||||
11() element: null
|
||||
8(10) READ ACCESS: y
|
||||
9(10) ASSERTTYPE ACCESS: null
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
+6
-5
@@ -2,11 +2,12 @@
|
||||
1(2) element: PyMatchStatement
|
||||
2(3) WRITE ACCESS: x
|
||||
3(4) element: PyExpressionStatement
|
||||
4(9) READ ACCESS: y
|
||||
4(10) READ ACCESS: y
|
||||
5(6,9) refutable pattern: 42
|
||||
6(7) matched pattern: 42
|
||||
7(8) element: PyExpressionStatement
|
||||
8(9) READ ACCESS: y
|
||||
9(10) element: PyExpressionStatement
|
||||
10(11) READ ACCESS: z
|
||||
11() element: null
|
||||
8(10) READ ACCESS: y
|
||||
9(10) ASSERTTYPE ACCESS: null
|
||||
10(11) element: PyExpressionStatement
|
||||
11(12) READ ACCESS: z
|
||||
12() element: null
|
||||
@@ -0,0 +1,521 @@
|
||||
# This sample tests type checking for match statements (as
|
||||
# described in PEP 634) that contain class patterns.
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
TypedDict,
|
||||
runtime_checkable,
|
||||
assert_type,
|
||||
Never
|
||||
)
|
||||
from typing_extensions import ( # pyright: ignore[reportMissingModuleSource]
|
||||
LiteralString,
|
||||
)
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
foo = 3
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class ClassA:
|
||||
__match_args__ = ("attr_a", "attr_b")
|
||||
attr_a: int
|
||||
attr_b: str
|
||||
|
||||
|
||||
class ClassB(Generic[T]):
|
||||
__match_args__ = ("attr_a", "attr_b")
|
||||
attr_a: T
|
||||
attr_b: str
|
||||
|
||||
|
||||
class ClassC: ...
|
||||
|
||||
|
||||
class ClassD(ClassC): ...
|
||||
|
||||
|
||||
def test_unknown(value_to_match):
|
||||
match value_to_match:
|
||||
case ClassA(attr_a=a2) as a1:
|
||||
assert_type(a1, ClassA)
|
||||
assert_type(a2, int)
|
||||
assert_type(value_to_match, ClassA)
|
||||
|
||||
# This should generate an error because foo isn't instantiable.
|
||||
case foo() as a3:
|
||||
pass
|
||||
|
||||
|
||||
def test_any(value_to_match: Any):
|
||||
match value_to_match:
|
||||
case list() as a1:
|
||||
assert_type(a1, list)
|
||||
assert_type(value_to_match, list)
|
||||
|
||||
|
||||
def test_custom_type(value_to_match: ClassA | ClassB[int] | ClassB[str] | ClassC):
|
||||
match value_to_match:
|
||||
case int() as a1:
|
||||
assert_type(a1, int)
|
||||
assert_type(value_to_match, int)
|
||||
|
||||
case ClassA(attr_a=a4, attr_b=a5) as a3:
|
||||
assert_type(a3, ClassA)
|
||||
assert_type(a4, int)
|
||||
assert_type(a5, str)
|
||||
assert_type(value_to_match, ClassA)
|
||||
assert_type(value_to_match, ClassA)
|
||||
|
||||
case ClassB(a6, a7):
|
||||
assert_type(a6, int | str)
|
||||
assert_type(a7, str)
|
||||
assert_type(value_to_match, ClassB[int] | ClassB[str])
|
||||
|
||||
case ClassD() as a2:
|
||||
assert_type(a2, ClassD)
|
||||
assert_type(value_to_match, ClassD)
|
||||
|
||||
case ClassC() as a8:
|
||||
assert_type(a8, ClassC)
|
||||
assert_type(value_to_match, ClassC)
|
||||
|
||||
|
||||
def test_subclass(value_to_match: ClassD):
|
||||
match value_to_match:
|
||||
case ClassC() as a1:
|
||||
assert_type(a1, ClassD)
|
||||
|
||||
case _ as a2:
|
||||
assert_type(a2, Never)
|
||||
|
||||
|
||||
def test_literal(value_to_match: Literal[3]):
|
||||
match value_to_match:
|
||||
case int() as a1:
|
||||
assert_type(a1, Literal[3])
|
||||
assert_type(value_to_match, Literal[3])
|
||||
|
||||
case float() as a2:
|
||||
assert_type(a2, Never)
|
||||
assert_type(value_to_match, Never)
|
||||
|
||||
case str() as a3:
|
||||
assert_type(a3, Never)
|
||||
assert_type(value_to_match, Never)
|
||||
|
||||
|
||||
def test_literal_string(value_to_match: LiteralString) -> None:
|
||||
match value_to_match:
|
||||
case "a" as a1:
|
||||
assert_type(value_to_match, Literal['a'])
|
||||
assert_type(a1, Literal['a'])
|
||||
case str() as a2:
|
||||
assert_type(value_to_match, LiteralString)
|
||||
assert_type(a2, LiteralString)
|
||||
case a3:
|
||||
assert_type(value_to_match, Never)
|
||||
assert_type(a3, Never)
|
||||
|
||||
|
||||
TFloat = TypeVar("TFloat", bound=float)
|
||||
|
||||
|
||||
def test_bound_typevar(value_to_match: TFloat) -> TFloat:
|
||||
match value_to_match:
|
||||
case int() as a1:
|
||||
assert_type(a1, int)
|
||||
assert_type(value_to_match, int)
|
||||
|
||||
case float() as a2:
|
||||
assert_type(a2, float)
|
||||
assert_type(value_to_match, float)
|
||||
|
||||
case str() as a3:
|
||||
assert_type(a3, str)
|
||||
assert_type(value_to_match, str)
|
||||
|
||||
return value_to_match
|
||||
|
||||
|
||||
# TInt = TypeVar("TInt", bound=int)
|
||||
|
||||
|
||||
# def test_union(
|
||||
# value_to_match: TInt | Literal[3] | float | str,
|
||||
# ) -> TInt | Literal[3] | float | str:
|
||||
# match value_to_match:
|
||||
# case int() as a1:
|
||||
# assert_type(a1, int)
|
||||
# assert_type(value_to_match, int)
|
||||
#
|
||||
# case float() as a2:
|
||||
# assert_type(a2, float)
|
||||
# assert_type(value_to_match, float)
|
||||
#
|
||||
# case str() as a3:
|
||||
# assert_type(a3, str)
|
||||
# assert_type(value_to_match, str)
|
||||
#
|
||||
# return value_to_match
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Point(Generic[T]):
|
||||
__match_args__ = ("x", "y")
|
||||
x: T
|
||||
y: T
|
||||
|
||||
|
||||
def func1(points: list[Point[float] | Point[complex]]):
|
||||
match points:
|
||||
case [] as a1:
|
||||
assert_type(a1, list[Point[float] | Point[complex]])
|
||||
assert_type(points, list[Point[float] | Point[complex]])
|
||||
|
||||
case [Point(0, 0) as b1]:
|
||||
assert_type(b1, Point[float] | Point[complex])
|
||||
assert_type(points, list[Point[float] | Point[complex]])
|
||||
|
||||
case [Point(c1, c2)]:
|
||||
assert_type(c1, float | complex)
|
||||
assert_type(c2, float | complex)
|
||||
assert_type(points, list[Point[float] | Point[complex]])
|
||||
|
||||
case [Point(0, d1), Point(0, d2)]:
|
||||
assert_type(d1, float | complex)
|
||||
assert_type(d2, float | complex)
|
||||
assert_type(points, list[Point[float] | Point[complex]])
|
||||
|
||||
case _ as e1:
|
||||
assert_type(e1, list[Point[float] | Point[complex]])
|
||||
assert_type(points, list[Point[float] | Point[complex]])
|
||||
|
||||
|
||||
def func2(subj: object):
|
||||
match subj:
|
||||
case list() as a1:
|
||||
assert_type(a1, list)
|
||||
assert_type(subj, list)
|
||||
|
||||
|
||||
def func3(subj: int | str | dict[str, str]):
|
||||
match subj:
|
||||
case int(x):
|
||||
assert_type(x, int)
|
||||
assert_type(subj, int)
|
||||
|
||||
case str(x):
|
||||
assert_type(x, str)
|
||||
assert_type(subj, str)
|
||||
|
||||
case dict(x):
|
||||
assert_type(x, dict[str, str])
|
||||
assert_type(subj, dict[str, str])
|
||||
|
||||
|
||||
def func4(subj: object):
|
||||
match subj:
|
||||
case int(x):
|
||||
assert_type(x, int)
|
||||
assert_type(subj, int)
|
||||
|
||||
case str(x):
|
||||
assert_type(x, str)
|
||||
assert_type(subj, str)
|
||||
|
||||
|
||||
# Test the auto-generation of __match_args__ for dataclass.
|
||||
@dataclass
|
||||
class Dataclass1:
|
||||
val1: int
|
||||
val2: str = field(init=False)
|
||||
val3: complex
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dataclass2:
|
||||
val1: int
|
||||
val2: str
|
||||
val3: float
|
||||
|
||||
|
||||
def func5(subj: object):
|
||||
match subj:
|
||||
case Dataclass1(a, b):
|
||||
assert_type(a, int)
|
||||
assert_type(b, complex)
|
||||
assert_type(subj, Dataclass1)
|
||||
|
||||
case Dataclass2(a, b, c):
|
||||
assert_type(a, int)
|
||||
assert_type(b, str)
|
||||
assert_type(c, float)
|
||||
assert_type(subj, Dataclass2)
|
||||
|
||||
|
||||
# # Test the auto-generation of __match_args__ for named tuples.
|
||||
# NT1 = NamedTuple("NT1", [("val1", int), ("val2", complex)])
|
||||
# NT2 = NamedTuple("NT2", [("val1", int), ("val2", str), ("val3", float)])
|
||||
#
|
||||
#
|
||||
# def func6(subj: object):
|
||||
# match subj:
|
||||
# case NT1(a, b):
|
||||
# assert_type(a, int)
|
||||
# assert_type(b, complex)
|
||||
# assert_type(subj, NT1)
|
||||
#
|
||||
# case NT2(a, b, c):
|
||||
# assert_type(a, int)
|
||||
# assert_type(b, str)
|
||||
# assert_type(c, float)
|
||||
# assert_type(subj, NT2)
|
||||
#
|
||||
#
|
||||
# def func7(subj: object):
|
||||
# match subj:
|
||||
# case complex(real=a, imag=b):
|
||||
# assert_type(a, float)
|
||||
# assert_type(b, float)
|
||||
|
||||
|
||||
# T2 = TypeVar("T2")
|
||||
#
|
||||
#
|
||||
# class Parent(Generic[T]): ...
|
||||
#
|
||||
#
|
||||
# class Child1(Parent[T]): ...
|
||||
#
|
||||
#
|
||||
# class Child2(Parent[T], Generic[T, T2]): ...
|
||||
#
|
||||
#
|
||||
# def func8(subj: Parent[int]):
|
||||
# match subj:
|
||||
# case Child1() as a1:
|
||||
# assert_type(a1, Child1[int])
|
||||
# assert_type(subj, Child1[int])
|
||||
#
|
||||
# case Child2() as b1:
|
||||
# assert_type(b1, Child2[int, Unknown])
|
||||
# assert_type(subj, Child2[int, Unknown])
|
||||
|
||||
|
||||
T3 = TypeVar("T3")
|
||||
|
||||
|
||||
def func9(v: T3) -> T3 | None:
|
||||
match v:
|
||||
case str():
|
||||
assert_type(v, str)
|
||||
return v
|
||||
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
T4 = TypeVar("T4", int, str)
|
||||
|
||||
|
||||
def func10(v: T4) -> T4 | None:
|
||||
match v:
|
||||
case str():
|
||||
assert_type(v, str)
|
||||
return v
|
||||
|
||||
case int():
|
||||
assert_type(v, int)
|
||||
return v
|
||||
|
||||
case list():
|
||||
assert_type(v, list)
|
||||
return v
|
||||
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
def func11(subj: Any):
|
||||
match subj:
|
||||
case Child1() as a1:
|
||||
assert_type(a1, Child1)
|
||||
assert_type(subj, Child1)
|
||||
|
||||
case Child2() as b1:
|
||||
assert_type(b1, Child2)
|
||||
assert_type(subj, Child2)
|
||||
|
||||
|
||||
class TD1(TypedDict):
|
||||
x: int
|
||||
|
||||
|
||||
def func12(subj: int, flt_cls: type[float], union_val: float | int):
|
||||
match subj:
|
||||
# This should generate an error because int doesn't accept two arguments.
|
||||
case int(1, 2):
|
||||
pass
|
||||
|
||||
match subj:
|
||||
# This should generate an error because float doesn't accept keyword arguments.
|
||||
case float(x=1):
|
||||
pass
|
||||
|
||||
match subj:
|
||||
case flt_cls():
|
||||
pass
|
||||
|
||||
# This should generate an error because it is a union.
|
||||
case union_val():
|
||||
pass
|
||||
|
||||
# This should generate an error because it is a TypedDict.
|
||||
case TD1():
|
||||
pass
|
||||
|
||||
|
||||
# def func13(subj: tuple[Literal[0]]):
|
||||
# match subj:
|
||||
# case tuple((1,)) as a:
|
||||
# assert_type(subj, Never)
|
||||
# assert_type(a, Never)
|
||||
#
|
||||
# case tuple((0, 0)) as b:
|
||||
# assert_type(subj, Never)
|
||||
# assert_type(b, Never)
|
||||
#
|
||||
# case tuple((0,)) as c:
|
||||
# assert_type(subj, tuple[Literal[0]])
|
||||
# assert_type(c, tuple[Literal[0]])
|
||||
#
|
||||
# case d:
|
||||
# assert_type(subj, Never)
|
||||
# assert_type(d, Never)
|
||||
|
||||
|
||||
# class ClassE(Generic[T]):
|
||||
# __match_args__ = ("x",)
|
||||
# x: list[T]
|
||||
#
|
||||
#
|
||||
# class ClassF(ClassE[T]):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# def func14(subj: ClassE[T]) -> T | None:
|
||||
# match subj:
|
||||
# case ClassF(a):
|
||||
# assert_type(subj, ClassF[T])
|
||||
# assert_type(a, list[T])
|
||||
# return a[0]
|
||||
#
|
||||
#
|
||||
# class IntPair(tuple[int, int]):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# def func15(x: IntPair | None) -> None:
|
||||
# match x:
|
||||
# case IntPair((y, z)):
|
||||
# assert_type(y, int)
|
||||
# assert_type(z, int)
|
||||
#
|
||||
#
|
||||
# def func16(x: str | float | bool | None):
|
||||
# match x:
|
||||
# case str(v) | bool(v) | float(v):
|
||||
# assert_type(v, str | bool | float)
|
||||
# assert_type(x, str | bool | float)
|
||||
# case v:
|
||||
# assert_type(v, int | None)
|
||||
# assert_type(x, int | None)
|
||||
# assert_type(x, str | bool | float | int | None)
|
||||
#
|
||||
#
|
||||
# def func17(x: str | float | bool | None):
|
||||
# match x:
|
||||
# case str() | float() | bool():
|
||||
# assert_type(x, str | float | bool)
|
||||
# case _:
|
||||
# assert_type(x, int | None)
|
||||
# assert_type(x, str | float | bool | int | None)
|
||||
#
|
||||
#
|
||||
# def func18(x: str | float | bool | None):
|
||||
# match x:
|
||||
# case str(v) | float(v) | bool(v):
|
||||
# assert_type(v, str | float | bool)
|
||||
# assert_type(x, str | float | bool)
|
||||
# case _:
|
||||
# assert_type(x, int | None)
|
||||
# assert_type(x, str | float | bool | int | None)
|
||||
#
|
||||
#
|
||||
# T5 = TypeVar("T5", complex, str)
|
||||
#
|
||||
#
|
||||
# def func19(x: T5) -> T5:
|
||||
# match x:
|
||||
# case complex():
|
||||
# return x
|
||||
# case str():
|
||||
# return x
|
||||
#
|
||||
# assert_type(x, float | int)
|
||||
# return x
|
||||
#
|
||||
#
|
||||
# T6 = TypeVar("T6", bound=complex | str)
|
||||
#
|
||||
#
|
||||
# def func20(x: T6) -> T6:
|
||||
# match x:
|
||||
# case complex():
|
||||
# return x
|
||||
# case str():
|
||||
# return x
|
||||
#
|
||||
# assert_type(x, float | int)
|
||||
# return x
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Proto1(Protocol):
|
||||
x: int
|
||||
|
||||
|
||||
class Proto2(Protocol):
|
||||
x: int
|
||||
|
||||
|
||||
def func21(subj: object):
|
||||
match subj:
|
||||
case Proto1():
|
||||
pass
|
||||
|
||||
# This should generate an error because Proto2 isn't runtime checkable.
|
||||
case Proto2():
|
||||
pass
|
||||
|
||||
|
||||
class Impl1:
|
||||
x: int
|
||||
|
||||
|
||||
def func22(subj: Proto1 | int):
|
||||
match subj:
|
||||
case Proto1():
|
||||
assert_type(subj, Proto1)
|
||||
|
||||
case _:
|
||||
assert_type(subj, int)
|
||||
@@ -14,6 +14,7 @@ classes_override.py
|
||||
constructors_call_init.py
|
||||
constructors_call_new.py
|
||||
constructors_call_type.py
|
||||
constructors_call_metaclass.py
|
||||
constructors_callable.py
|
||||
dataclasses_hash.py
|
||||
dataclasses_inheritance.py
|
||||
@@ -65,4 +66,5 @@ specialtypes_promotions.py
|
||||
specialtypes_type.py
|
||||
tuples_type_form.py
|
||||
tuples_unpacked.py
|
||||
tuples_type_compat.py
|
||||
typeddicts_readonly_inheritance.py
|
||||
@@ -13,6 +13,17 @@ public class PyPatternTypeTest extends PyInspectionTestCase {
|
||||
return PyAssertTypeInspection.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTestCaseDirectory() {
|
||||
return "inspections/PyPatternTypeTest/";
|
||||
}
|
||||
|
||||
public void testPyrightMatchClass1() {
|
||||
// matchClass1.py test from pyright repo, adjusted to our system.
|
||||
// Parts that are commented out are WIP
|
||||
doTest();
|
||||
}
|
||||
|
||||
public void testMatchCapturePatternType() {
|
||||
doTestByText("""
|
||||
from typing import assert_type
|
||||
@@ -310,6 +321,54 @@ match m:
|
||||
assert_type(m, list[list[str]])
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchSequenceNotNamedElement() {
|
||||
doTestByText("""
|
||||
from typing import assert_type
|
||||
def func():
|
||||
match [10]:
|
||||
case [*values]:
|
||||
return values[0]
|
||||
|
||||
assert_type(func(), int)
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchHomogeneousTuplePattern() {
|
||||
doTestByText("""
|
||||
from typing import assert_type
|
||||
m: tuple[int, ...]
|
||||
|
||||
match m:
|
||||
case [a, b, c]:
|
||||
assert_type(a, int)
|
||||
assert_type(b, int)
|
||||
assert_type(c, int)
|
||||
assert_type(m, tuple[int, ...])
|
||||
|
||||
case [a, b]:
|
||||
assert_type(a, int)
|
||||
assert_type(b, int)
|
||||
assert_type(m, tuple[int, ...])
|
||||
""");
|
||||
}
|
||||
|
||||
|
||||
public void testMatchNotHomogeneousTuplePattern() {
|
||||
doTestByText("""
|
||||
from typing import assert_type, Never
|
||||
m: tuple[int, str]
|
||||
|
||||
match m:
|
||||
case [a, b]:
|
||||
assert_type(a, int)
|
||||
assert_type(b, str)
|
||||
assert_type(m, tuple[int, str])
|
||||
|
||||
case x:
|
||||
assert_type(x, Never)
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchMappingPatternCaptures() {
|
||||
doTestByText("""
|
||||
@@ -356,6 +415,30 @@ match m:
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchMappingPatternCapturesUnion() {
|
||||
doTestByText("""
|
||||
from typing import TypedDict, Literal, assert_type
|
||||
|
||||
class A(TypedDict):
|
||||
a: Literal["str"]
|
||||
b: Literal[42]
|
||||
|
||||
|
||||
m: A | dict[str | int, int]
|
||||
|
||||
match m:
|
||||
case {"a": v}:
|
||||
assert_type(v, Literal["str"] | int)
|
||||
case {"b": v2}:
|
||||
assert_type(v2, Literal[42] | int)
|
||||
case {"a": v3, "b": v4}:
|
||||
assert_type(v3, Literal["str"] | int)
|
||||
assert_type(v4, Literal[42] | int)
|
||||
case {42: v5}:
|
||||
assert_type(v5, int)
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchMappingPatternCaptureRest() {
|
||||
doTestByText("""
|
||||
from typing import Mapping, assert_type
|
||||
@@ -403,9 +486,11 @@ class A:
|
||||
m: A
|
||||
|
||||
match m:
|
||||
case A(a=i, b=j):
|
||||
assert_type(i, str)
|
||||
case A(a="name", b=j):
|
||||
assert_type(j, int)
|
||||
|
||||
case A():
|
||||
assert_type(m, A)
|
||||
""");
|
||||
}
|
||||
|
||||
@@ -424,6 +509,25 @@ def f(x):
|
||||
assert_type(x, str)
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchClassPatternNegativeNarrow() {
|
||||
doTestByText("""
|
||||
from typing import assert_type, Never
|
||||
|
||||
class A:
|
||||
a: str
|
||||
b: int
|
||||
|
||||
m: A
|
||||
|
||||
match m:
|
||||
case A(a=i, b=j):
|
||||
assert_type(j, int)
|
||||
|
||||
case A():
|
||||
assert_type(m, Never)
|
||||
""");
|
||||
}
|
||||
|
||||
public void testMatchClassPatternCaptureSelf() {
|
||||
doTestByText("""
|
||||
@@ -569,6 +673,7 @@ match x:
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-79832
|
||||
public void testMatchClassPatternSelfCaptureParameterized() {
|
||||
doTestByText("""
|
||||
def flip(pair: list[int]):
|
||||
@@ -724,4 +829,53 @@ def func(val: tuple[*tuple[str, ...], *tuple[int, ...]]):
|
||||
assert_type(val, tuple[str, *tuple[int, ...]])
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
// PY-53880
|
||||
public void testGuardConditionDoesNotExcludePattern() {
|
||||
doTestByText("""
|
||||
from typing import assert_type
|
||||
|
||||
|
||||
def excluding_conditional_pattern(p: int):
|
||||
match p:
|
||||
case int() if p >= 0:
|
||||
pass
|
||||
case negative:
|
||||
assert_type(negative, int)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-53880
|
||||
public void testClassPatternTypeTypeNotExhaustive() {
|
||||
doTestByText("""
|
||||
from typing import assert_type, Never
|
||||
|
||||
class A:
|
||||
pass
|
||||
|
||||
def check_pattern(clazz: type[A], obj: A):
|
||||
match obj:
|
||||
case clazz(): # <-- this is not exhaustive
|
||||
assert_type(obj, A)
|
||||
case A():
|
||||
assert_type(obj, A)
|
||||
case _:
|
||||
assert_type(obj, Never)
|
||||
""");
|
||||
}
|
||||
|
||||
// PY-53880
|
||||
public void testListNotExhaustive() {
|
||||
doTestByText("""
|
||||
from typing import assert_type
|
||||
|
||||
|
||||
def excluding_fixed_size_list(p: list[str]):
|
||||
match p:
|
||||
case [*items, item1, item2]:
|
||||
pass
|
||||
case shorter_than_two:
|
||||
assert_type(shorter_than_two, list[str])
|
||||
""");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user