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:
Aleksandr.Govenko
2025-05-30 18:33:26 +00:00
committed by intellij-monorepo-bot
parent 7216e0d719
commit b54f5b4ae8
48 changed files with 1189 additions and 254 deletions
@@ -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;
}
}
@@ -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()));
}
}
@@ -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) {
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
+2
View File
@@ -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])
""");
}
}