Files
TermiNexus e9e4693333
Some checks failed
Run mypy_primer on push / Run mypy_primer on push (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
初始化上传
2026-07-24 17:08:39 +08:00

2320 lines
96 KiB
TypeScript

/*
* patternMatching.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
* Author: Eric Traut
*
* Type evaluation logic for evaluating and narrowing types
* related to "match" and "case" statements as documented in
* PEP 634.
*/
import { appendArray } from '../common/collectionUtils';
import { assert } from '../common/debug';
import { DiagnosticAddendum } from '../common/diagnostic';
import { DiagnosticRule } from '../common/diagnosticRules';
import { LocAddendum, LocMessage } from '../localization/localize';
import {
ArgCategory,
ExpressionNode,
ParseNode,
ParseNodeType,
PatternAsNode,
PatternAtomNode,
PatternClassArgumentNode,
PatternClassNode,
PatternLiteralNode,
PatternMappingNode,
PatternSequenceNode,
PatternValueNode,
} from '../parser/parseNodes';
import { CodeFlowReferenceExpressionNode } from './codeFlowTypes';
import { addConstraintsForExpectedType } from './constraintSolver';
import { ConstraintTracker } from './constraintTracker';
import { getTypeVarScopesForNode, isMatchingExpression } from './parseTreeUtils';
import { getTypedDictMembersForClass } from './typedDicts';
import { EvalFlags, TypeEvaluator, TypeResult } from './typeEvaluatorTypes';
import {
enumerateLiteralsForType,
narrowTypeForDiscriminatedDictEntryComparison,
narrowTypeForDiscriminatedLiteralFieldComparison,
narrowTypeForDiscriminatedTupleComparison,
} from './typeGuards';
import {
AnyType,
ClassType,
FunctionType,
FunctionTypeFlags,
NeverType,
Type,
TypeBase,
TypedDictEntry,
UnknownType,
combineTypes,
isAnyOrUnknown,
isClass,
isClassInstance,
isInstantiableClass,
isNever,
isSameWithoutLiteralValue,
isTypeSame,
isTypeVar,
isTypeVarTuple,
isUnknown,
isUnpackedTypeVar,
isUnpackedTypeVarTuple,
} from './types';
import {
addConditionToType,
containsAnyOrUnknown,
convertToInstance,
doForEachSubtype,
getTypeCondition,
getTypeVarScopeIds,
getUnknownForTypeVar,
getUnknownTypeForCallable,
isLiteralType,
isLiteralTypeOrUnion,
isMetaclassInstance,
isNoneInstance,
isPartlyUnknown,
isTupleClass,
isUnboundedTupleClass,
lookUpClassMember,
mapSubtypes,
partiallySpecializeType,
preserveUnknown,
specializeTupleClass,
specializeWithUnknownTypeArgs,
transformPossibleRecursiveTypeAlias,
} from './typeUtils';
// PEP 634 indicates that several built-in classes are handled differently
// when used with class pattern matching.
const classPatternSpecialCases = [
'builtins.bool',
'builtins.bytearray',
'builtins.bytes',
'builtins.dict',
'builtins.float',
'builtins.frozenset',
'builtins.int',
'builtins.list',
'builtins.set',
'builtins.str',
'builtins.tuple',
];
// There are cases where sequence pattern matching of tuples with
// large unions can blow up and cause hangs. This constant limits
// the total number of subtypes that can be generated during type
// narrowing for sequence patterns before the narrowed type is
// converted to Any. This is tuned empirically to provide a reasonable
// performance cutoff.
const maxSequencePatternTupleExpansionSubtypes = 128;
interface SequencePatternInfo {
subtype: Type;
isDefiniteNoMatch: boolean;
isPotentialNoMatch?: boolean;
entryTypes: Type[];
isIndeterminateLength?: boolean;
isTuple?: boolean;
isUnboundedTuple?: boolean;
}
interface MappingPatternInfo {
subtype: Type;
isDefinitelyMapping: boolean;
isDefinitelyNotMapping: boolean;
typedDict?: ClassType;
dictTypeArgs?: {
key: Type;
value: Type;
};
}
type PatternSubtypeNarrowingCallback = (type: Type) => TypeResult | undefined;
export function narrowTypeBasedOnPattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternAtomNode,
isPositiveTest: boolean
): Type {
switch (pattern.nodeType) {
case ParseNodeType.PatternSequence: {
return narrowTypeBasedOnSequencePattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternLiteral: {
return narrowTypeBasedOnLiteralPattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternClass: {
return narrowTypeBasedOnClassPattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternAs: {
return narrowTypeBasedOnAsPattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternMapping: {
return narrowTypeBasedOnMappingPattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternValue: {
return narrowTypeBasedOnValuePattern(evaluator, type, pattern, isPositiveTest);
}
case ParseNodeType.PatternCapture: {
// A capture captures everything, so nothing remains in the negative case.
return isPositiveTest ? type : NeverType.createNever();
}
case ParseNodeType.Error: {
return type;
}
}
}
// Determines whether this pattern (or part of the pattern) in
// this case statement will never be matched.
export function checkForUnusedPattern(evaluator: TypeEvaluator, pattern: PatternAtomNode, subjectType: Type): void {
if (isNever(subjectType)) {
reportUnnecessaryPattern(evaluator, pattern, subjectType);
} else if (pattern.nodeType === ParseNodeType.PatternAs && pattern.d.orPatterns.length > 1) {
// Check each of the or patterns separately.
pattern.d.orPatterns.forEach((orPattern) => {
const subjectTypeMatch = narrowTypeBasedOnPattern(
evaluator,
subjectType,
orPattern,
/* isPositiveTest */ true
);
if (isNever(subjectTypeMatch)) {
reportUnnecessaryPattern(evaluator, orPattern, subjectType);
}
subjectType = narrowTypeBasedOnPattern(evaluator, subjectType, orPattern, /* isPositiveTest */ false);
});
} else {
const subjectTypeMatch = narrowTypeBasedOnPattern(evaluator, subjectType, pattern, /* isPositiveTest */ true);
if (isNever(subjectTypeMatch)) {
reportUnnecessaryPattern(evaluator, pattern, subjectType);
}
}
}
function narrowTypeBasedOnSequencePattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternSequenceNode,
isPositiveTest: boolean
): Type {
let usingTupleExpansion = false;
type = transformPossibleRecursiveTypeAlias(type);
let sequenceInfo = getSequencePatternInfo(evaluator, pattern, type);
// Further narrow based on pattern entry types.
sequenceInfo = sequenceInfo.filter((entry) => {
if (entry.isDefiniteNoMatch) {
if (isPositiveTest) {
return false;
} else {
return true;
}
}
let isPlausibleMatch = true;
let isDefiniteMatch = true;
const narrowedEntryTypes: Type[] = [];
const unnarrowedEntryTypes: Type[] = [];
let canNarrowTuple = entry.isTuple;
// Don't attempt to narrow tuples in the negative case if the subject
// contains indeterminate-length entries or the tuple is of indeterminate
// length.
if (!isPositiveTest) {
if (entry.isIndeterminateLength || entry.isUnboundedTuple) {
canNarrowTuple = false;
}
if (isClassInstance(entry.subtype) && entry.subtype.priv.tupleTypeArgs) {
const unboundedIndex = entry.subtype.priv.tupleTypeArgs.findIndex((typeArg) => typeArg.isUnbounded);
if (unboundedIndex >= 0) {
// If the pattern includes a "star" entry that aligns exactly with
// the corresponding unbounded entry in the tuple, we can narrow
// the tuple type.
if (pattern.d.starEntryIndex === undefined || pattern.d.starEntryIndex !== unboundedIndex) {
canNarrowTuple = false;
}
}
}
}
// If the subject has an indeterminate length but the pattern does not accept
// an arbitrary number of entries or accepts at least one non-star entry,
// we can't prove that it's a definite match.
if (entry.isIndeterminateLength) {
if (pattern.d.entries.length !== 1 || pattern.d.starEntryIndex !== 0) {
isDefiniteMatch = false;
}
}
const negativeNarrowedDims: number[] = [];
pattern.d.entries.forEach((sequenceEntry, index) => {
const entryType = getTypeOfPatternSequenceEntry(
evaluator,
pattern,
entry,
index,
pattern.d.entries.length,
pattern.d.starEntryIndex,
/* unpackStarEntry */ true
);
unnarrowedEntryTypes.push(entryType);
const narrowedEntryType = narrowTypeBasedOnPattern(evaluator, entryType, sequenceEntry, isPositiveTest);
if (isPositiveTest) {
if (index === pattern.d.starEntryIndex) {
if (
isClassInstance(narrowedEntryType) &&
narrowedEntryType.priv.tupleTypeArgs &&
!isUnboundedTupleClass(narrowedEntryType) &&
narrowedEntryType.priv.tupleTypeArgs
) {
appendArray(
narrowedEntryTypes,
narrowedEntryType.priv.tupleTypeArgs.map((t) => t.type)
);
} else {
narrowedEntryTypes.push(narrowedEntryType);
canNarrowTuple = false;
}
} else {
narrowedEntryTypes.push(narrowedEntryType);
if (isNever(narrowedEntryType)) {
isPlausibleMatch = false;
}
}
} else {
if (entry.isPotentialNoMatch) {
isDefiniteMatch = false;
}
if (!isNever(narrowedEntryType)) {
isDefiniteMatch = false;
// Record which entries were narrowed in the negative case
// by storing their indexes. If more than one is narrowed,
// we need to perform tuple expansion to represent the
// resulting narrowed type.
negativeNarrowedDims.push(index);
narrowedEntryTypes.push(narrowedEntryType);
} else {
narrowedEntryTypes.push(entryType);
}
if (index === pattern.d.starEntryIndex) {
canNarrowTuple = false;
}
}
});
if (pattern.d.entries.length === 0) {
// If the pattern is an empty sequence, use the entry types.
if (entry.entryTypes.length > 0) {
narrowedEntryTypes.push(combineTypes(entry.entryTypes));
}
if (entry.isPotentialNoMatch) {
isDefiniteMatch = false;
}
}
if (!isPositiveTest) {
// If the positive case is a definite match, the negative case can
// eliminate this subtype entirely.
if (isDefiniteMatch) {
return false;
}
// Can we narrow a tuple?
if (canNarrowTuple && negativeNarrowedDims.length > 0) {
const tupleClassType = evaluator.getBuiltInType(pattern, 'tuple');
if (tupleClassType && isInstantiableClass(tupleClassType)) {
// Expand the tuple in the dimensions that were narrowed.
// Start with the fully-narrowed set of entries.
const expandedEntryTypes = [];
for (const dim of negativeNarrowedDims) {
const newEntryTypes = [...unnarrowedEntryTypes];
newEntryTypes[dim] = narrowedEntryTypes[dim];
expandedEntryTypes.push(newEntryTypes);
}
entry.subtype = combineTypes(
expandedEntryTypes.map((entryTypes) => {
return ClassType.cloneAsInstance(
specializeTupleClass(
tupleClassType,
entryTypes.map((t) => {
return { type: t, isUnbounded: false };
})
)
);
})
);
// Note that we're using tuple expansion in case we
// need to limit the number of subtypes generated.
usingTupleExpansion = true;
}
}
return true;
}
if (isPlausibleMatch) {
// If this is a tuple, we can narrow it to a specific tuple type.
// Other sequences cannot be narrowed because we don't know if they
// are immutable (covariant).
if (canNarrowTuple) {
const tupleClassType = evaluator.getBuiltInType(pattern, 'tuple');
if (tupleClassType && isInstantiableClass(tupleClassType)) {
entry.subtype = ClassType.cloneAsInstance(
specializeTupleClass(
tupleClassType,
narrowedEntryTypes.map((t) => {
return { type: t, isUnbounded: false };
})
)
);
}
}
// If this is a supertype of Sequence, we can narrow it to a Sequence type.
if (entry.isPotentialNoMatch && !entry.isTuple) {
const sequenceType = evaluator.getTypingType(pattern, 'Sequence');
if (sequenceType && isInstantiableClass(sequenceType)) {
let typeArgType = evaluator.stripLiteralValue(combineTypes(narrowedEntryTypes));
// If the type is a union that contains Any or Unknown, remove the other types
// before wrapping it in a Sequence.
typeArgType = containsAnyOrUnknown(typeArgType, /* recurse */ false) ?? typeArgType;
entry.subtype = ClassType.cloneAsInstance(ClassType.specialize(sequenceType, [typeArgType]));
}
}
}
return isPlausibleMatch;
});
return combineTypes(
sequenceInfo.map((entry) => entry.subtype),
{ maxSubtypeCount: usingTupleExpansion ? maxSequencePatternTupleExpansionSubtypes : undefined }
);
}
function narrowTypeBasedOnAsPattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternAsNode,
isPositiveTest: boolean
): Type {
let remainingType = type;
if (!isPositiveTest) {
pattern.d.orPatterns.forEach((subpattern) => {
remainingType = narrowTypeBasedOnPattern(evaluator, remainingType, subpattern, /* isPositiveTest */ false);
});
return remainingType;
}
const narrowedTypes = pattern.d.orPatterns.map((subpattern) => {
const narrowedSubtype = narrowTypeBasedOnPattern(
evaluator,
remainingType,
subpattern,
/* isPositiveTest */ true
);
remainingType = narrowTypeBasedOnPattern(evaluator, remainingType, subpattern, /* isPositiveTest */ false);
return narrowedSubtype;
});
return combineTypes(narrowedTypes);
}
function narrowTypeBasedOnMappingPattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternMappingNode,
isPositiveTest: boolean
): Type {
type = transformPossibleRecursiveTypeAlias(type);
if (!isPositiveTest) {
// Handle the case where the pattern consists only of a "**x" entry.
if (
pattern.d.entries.length === 1 &&
pattern.d.entries[0].nodeType === ParseNodeType.PatternMappingExpandEntry
) {
const mappingInfo = getMappingPatternInfo(evaluator, type, pattern);
return combineTypes(mappingInfo.filter((m) => !m.isDefinitelyMapping).map((m) => m.subtype));
}
if (pattern.d.entries.length !== 1 || pattern.d.entries[0].nodeType !== ParseNodeType.PatternMappingKeyEntry) {
return type;
}
// Handle the case where the type is a union that includes a TypedDict with
// a field discriminated by a literal.
const keyPattern = pattern.d.entries[0].d.keyPattern;
const valuePattern = pattern.d.entries[0].d.valuePattern;
if (
keyPattern.nodeType !== ParseNodeType.PatternLiteral ||
valuePattern.nodeType !== ParseNodeType.PatternAs ||
!valuePattern.d.orPatterns.every((orPattern) => orPattern.nodeType === ParseNodeType.PatternLiteral)
) {
return type;
}
const keyType = evaluator.getTypeOfExpression(keyPattern.d.expr).type;
// The key type must be a str literal.
if (
!isClassInstance(keyType) ||
!ClassType.isBuiltIn(keyType, 'str') ||
keyType.priv.literalValue === undefined
) {
return type;
}
const keyValue = keyType.priv.literalValue as string;
const valueTypes = valuePattern.d.orPatterns.map(
(orPattern) => evaluator.getTypeOfExpression((orPattern as PatternLiteralNode).d.expr).type
);
return mapSubtypes(type, (subtype) => {
if (isClassInstance(subtype) && ClassType.isTypedDictClass(subtype)) {
const typedDictMembers = getTypedDictMembersForClass(evaluator, subtype, /* allowNarrowed */ true);
const member = typedDictMembers.knownItems.get(keyValue);
if (member && (member.isRequired || member.isProvided) && isClassInstance(member.valueType)) {
const memberValueType = member.valueType;
// If there's at least one literal value pattern that matches
// the literal type of the member, we can eliminate this type.
if (
valueTypes.some(
(valueType) =>
isClassInstance(valueType) &&
ClassType.isSameGenericClass(valueType, memberValueType) &&
valueType.priv.literalValue === memberValueType.priv.literalValue
)
) {
return undefined;
}
}
}
return subtype;
});
}
let mappingInfo = getMappingPatternInfo(evaluator, type, pattern);
// Further narrow based on pattern entry types.
mappingInfo = mappingInfo.filter((mappingSubtypeInfo) => {
if (mappingSubtypeInfo.isDefinitelyNotMapping) {
return false;
}
let isPlausibleMatch = true;
pattern.d.entries.forEach((mappingEntry) => {
if (mappingSubtypeInfo.typedDict) {
if (mappingEntry.nodeType === ParseNodeType.PatternMappingKeyEntry) {
const narrowedKeyType = narrowTypeBasedOnPattern(
evaluator,
evaluator.getBuiltInObject(pattern, 'str'),
mappingEntry.d.keyPattern,
isPositiveTest
);
if (isNever(narrowedKeyType)) {
isPlausibleMatch = false;
}
const valueType = mapSubtypes(narrowedKeyType, (keySubtype) => {
if (isAnyOrUnknown(keySubtype)) {
return keySubtype;
}
if (isClassInstance(keySubtype) && ClassType.isBuiltIn(keySubtype, 'str')) {
if (!isLiteralType(keySubtype)) {
return UnknownType.create();
}
const tdEntries = getTypedDictMembersForClass(evaluator, mappingSubtypeInfo.typedDict!);
const valueEntry = tdEntries.knownItems.get(keySubtype.priv.literalValue as string);
if (valueEntry) {
const narrowedValueType = narrowTypeBasedOnPattern(
evaluator,
valueEntry.valueType,
mappingEntry.d.valuePattern,
/* isPositiveTest */ true
);
if (!isNever(narrowedValueType)) {
// If this is a "NotRequired" entry that has not yet been demonstrated
// to be present, we can mark it as "provided" at this point.
if (
!valueEntry.isRequired &&
!valueEntry.isProvided &&
isTypeSame(mappingSubtypeInfo.subtype, mappingSubtypeInfo.typedDict!)
) {
const newNarrowedEntriesMap = new Map<string, TypedDictEntry>(
mappingSubtypeInfo.typedDict!.priv.typedDictNarrowedEntries ?? []
);
newNarrowedEntriesMap.set(keySubtype.priv.literalValue as string, {
valueType: valueEntry.valueType,
isReadOnly: valueEntry.isReadOnly,
isRequired: false,
isProvided: true,
});
// Clone the TypedDict object with the new entries.
mappingSubtypeInfo.subtype = ClassType.cloneAsInstance(
ClassType.cloneForNarrowedTypedDictEntries(
ClassType.cloneAsInstantiable(mappingSubtypeInfo.typedDict!),
newNarrowedEntriesMap
)
);
mappingSubtypeInfo.typedDict = mappingSubtypeInfo.subtype;
}
return narrowedValueType;
}
}
}
return undefined;
});
if (isNever(valueType)) {
isPlausibleMatch = false;
}
}
} else if (mappingSubtypeInfo.dictTypeArgs) {
if (mappingEntry.nodeType === ParseNodeType.PatternMappingKeyEntry) {
const narrowedKeyType = narrowTypeBasedOnPattern(
evaluator,
mappingSubtypeInfo.dictTypeArgs.key,
mappingEntry.d.keyPattern,
isPositiveTest
);
const narrowedValueType = narrowTypeBasedOnPattern(
evaluator,
mappingSubtypeInfo.dictTypeArgs.value,
mappingEntry.d.valuePattern,
isPositiveTest
);
if (isNever(narrowedKeyType) || isNever(narrowedValueType)) {
isPlausibleMatch = false;
}
}
}
});
return isPlausibleMatch;
});
return combineTypes(mappingInfo.map((entry) => entry.subtype));
}
// Looks up the "__match_args__" class member to determine the names of
// the attributes used for class pattern matching.
function getPositionalMatchArgNames(evaluator: TypeEvaluator, type: ClassType): string[] {
const matchArgsMemberInfo = lookUpClassMember(type, '__match_args__');
if (matchArgsMemberInfo) {
const matchArgsType = evaluator.getTypeOfMember(matchArgsMemberInfo);
if (
isClassInstance(matchArgsType) &&
isTupleClass(matchArgsType) &&
!isUnboundedTupleClass(matchArgsType) &&
matchArgsType.priv.tupleTypeArgs
) {
const tupleArgs = matchArgsType.priv.tupleTypeArgs;
// Are all the args string literals?
if (
tupleArgs.every(
(arg) =>
isClassInstance(arg.type) && ClassType.isBuiltIn(arg.type, 'str') && isLiteralType(arg.type)
)
) {
return tupleArgs.map((arg) => (arg.type as ClassType).priv.literalValue as string);
}
}
}
return [];
}
function narrowTypeBasedOnLiteralPattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternLiteralNode,
isPositiveTest: boolean
): Type {
const literalType = evaluator.getTypeOfExpression(pattern.d.expr).type;
if (!isPositiveTest) {
return evaluator.mapSubtypesExpandTypeVars(type, /* options */ undefined, (expandedSubtype) => {
if (
isClassInstance(literalType) &&
isLiteralType(literalType) &&
isClassInstance(expandedSubtype) &&
isLiteralType(expandedSubtype) &&
evaluator.assignType(literalType, expandedSubtype)
) {
return undefined;
}
if (isNoneInstance(expandedSubtype) && isNoneInstance(literalType)) {
return undefined;
}
// Narrow a non-literal bool based on a literal bool pattern.
if (
isClassInstance(expandedSubtype) &&
ClassType.isBuiltIn(expandedSubtype, 'bool') &&
expandedSubtype.priv.literalValue === undefined &&
isClassInstance(literalType) &&
ClassType.isBuiltIn(literalType, 'bool') &&
literalType.priv.literalValue !== undefined
) {
return ClassType.cloneWithLiteral(literalType, !(literalType.priv.literalValue as boolean));
}
return expandedSubtype;
});
}
return evaluator.mapSubtypesExpandTypeVars(type, /* options */ undefined, (expandedSubtype, unexpandedSubtype) => {
if (evaluator.assignType(expandedSubtype, literalType)) {
// We have to be careful here because the runtime uses an equality
// check, but the expandedSubtype could be a superclass that is not
// the literal type. For example, the expanded subtype might be float
// and the literal type is Literal[3]. A value of 3.0 will match this
// pattern, but we cannot narrow it to Literal[3] in this case.
if (
!isClassInstance(literalType) ||
!isLiteralType(literalType) ||
isTypeSame(evaluator.stripLiteralValue(expandedSubtype), evaluator.stripLiteralValue(literalType))
) {
return literalType;
}
return expandedSubtype;
}
// See if the subtype is a subclass of the literal's class. For example,
// if it's a literal str, see if the subtype is subclass of str.
if (isClassInstance(literalType) && isClassInstance(expandedSubtype)) {
if (isLiteralType(literalType) && !isLiteralType(expandedSubtype)) {
if (
evaluator.assignType(
ClassType.cloneWithLiteral(literalType, /* value */ undefined),
expandedSubtype
)
) {
return expandedSubtype;
}
} else if (evaluator.assignType(literalType, expandedSubtype)) {
return expandedSubtype;
}
}
return undefined;
});
}
// When a class pattern matches a generic class whose type parameters have an
// upper bound (e.g. `class Thing[T: bool]`), the constraint solver may leave the
// parameters unsolved (Unknown) because the subject carries no type arguments.
// Rather than surfacing Unknown, fall back to each parameter's bound while
// preserving any argument that was concretely solved. The subject's condition is
// reapplied to the resulting instance.
function specializeBoundedMatchTypeParams(
evaluator: TypeEvaluator,
matchType: ClassType,
solvedTypeArgs: Type[] | undefined,
condition: ReturnType<typeof getTypeCondition>
): Type {
// `solvedTypeArgs` is indexed by `matchType`'s own type parameters, so the
// caller must align it to the pattern class (e.g. pass `resultType.priv.typeArgs`
// where `resultType` is the same generic class). The subject's own type arguments
// must not be used here: the subject may be a different generic class (e.g. a
// generic supertype), and indexing it by the pattern class's parameters would
// misread unrelated arguments.
const typeArgs = matchType.shared.typeParams.map((param, index) => {
const specializedArg = solvedTypeArgs?.[index];
// Keep an argument unless it is the unsolved sentinel (a bare top-level
// Unknown left by the constraint solver when the subject carried no type
// arguments). A concrete argument that is merely implicitly parameterized -
// e.g. bare `list` (`list[Unknown]`) or `dict` (`dict[Unknown, Unknown]`) - is
// a real, solved type and must be preserved rather than widened to the bound.
// A bare in-scope TypeVar (e.g. a subject `Thing[S]` from a generic function
// `def f[S: bool]`) is likewise a legitimately narrowed argument.
if (specializedArg && !isUnknown(specializedArg)) {
return specializedArg;
}
if (isTypeVar(param) && param.shared.boundType) {
return convertToInstance(param.shared.boundType);
}
return specializedArg ?? getUnknownForTypeVar(param, evaluator.getTupleClassType());
});
return addConditionToType(convertToInstance(ClassType.specialize(matchType, typeArgs)), condition);
}
function narrowTypeBasedOnClassPattern(
evaluator: TypeEvaluator,
type: Type,
pattern: PatternClassNode,
isPositiveTest: boolean
): Type {
let exprType = evaluator.getTypeOfExpression(pattern.d.className, EvalFlags.CallBaseDefaults).type;
// If this is a class (but not a type alias that refers to a class),
// specialize it with Unknown type arguments.
if (isClass(exprType) && !exprType.props?.typeAliasInfo) {
exprType = ClassType.cloneRemoveTypePromotions(exprType);
exprType = specializeWithUnknownTypeArgs(exprType, evaluator.getTupleClassType());
}
// Are there any positional arguments? If so, try to get the mappings for
// these arguments by fetching the __match_args__ symbol from the class.
let positionalArgNames: string[] = [];
if (pattern.d.args.some((arg) => !arg.d.name) && isInstantiableClass(exprType)) {
positionalArgNames = getPositionalMatchArgNames(evaluator, exprType);
}
if (!isPositiveTest) {
// Don't attempt to narrow if the class type is a more complex type (e.g. a TypeVar or union).
if (!isInstantiableClass(exprType)) {
return type;
}
let classType = exprType;
if (classType.shared.typeParams.length > 0) {
classType = ClassType.specialize(classType, /* typeArgs */ undefined);
}
const classInstance = ClassType.cloneAsInstance(classType);
const isPatternMetaclass = isMetaclassInstance(classInstance);
return evaluator.mapSubtypesExpandTypeVars(
type,
{
expandCallback: (type) => evaluator.expandPromotionTypes(pattern, type),
},
(subjectSubtypeExpanded, subjectSubtypeUnexpanded) => {
// Handle the case where the class pattern references type() or a subtype thereof
// and the subject type is an instantiable class itself.
if (isPatternMetaclass && isInstantiableClass(subjectSubtypeExpanded)) {
const metaclass = subjectSubtypeExpanded.shared.effectiveMetaclass ?? UnknownType.create();
if (isInstantiableClass(classType) && evaluator.assignType(classType, metaclass)) {
return undefined;
}
return subjectSubtypeExpanded;
}
// Handle Callable specially.
if (
!isAnyOrUnknown(subjectSubtypeExpanded) &&
isInstantiableClass(classType) &&
ClassType.isBuiltIn(classType, 'Callable')
) {
if (evaluator.assignType(getUnknownTypeForCallable(), subjectSubtypeExpanded)) {
return undefined;
}
}
if (!isNoneInstance(subjectSubtypeExpanded) && !isClassInstance(subjectSubtypeExpanded)) {
return subjectSubtypeUnexpanded;
}
// Handle NoneType specially.
if (
isNoneInstance(subjectSubtypeExpanded) &&
isInstantiableClass(classType) &&
ClassType.isBuiltIn(classType, 'NoneType')
) {
return undefined;
}
if (!evaluator.assignType(classInstance, subjectSubtypeExpanded)) {
return subjectSubtypeExpanded;
}
// Handle literal types specially.
if (isClassInstance(subjectSubtypeExpanded) && isLiteralType(subjectSubtypeExpanded)) {
return undefined;
}
if (pattern.d.args.length === 0) {
if (isClass(classInstance) && isClass(subjectSubtypeExpanded)) {
// We know that this match will always succeed, so we can
// eliminate this subtype.
return undefined;
}
return subjectSubtypeExpanded;
}
// We might be able to narrow further based on arguments, but only
// if the types match exactly, the subject subtype is a final class (and
// therefore cannot be subclassed), or the pattern class is a protocol
// class.
if (!evaluator.assignType(subjectSubtypeExpanded, classInstance)) {
if (
isClass(subjectSubtypeExpanded) &&
!ClassType.isFinal(subjectSubtypeExpanded) &&
!ClassType.isProtocolClass(classInstance)
) {
return subjectSubtypeExpanded;
}
}
for (let index = 0; index < pattern.d.args.length; index++) {
const narrowedArgType = narrowTypeOfClassPatternArg(
evaluator,
pattern.d.args[index],
index,
positionalArgNames,
subjectSubtypeExpanded,
isPositiveTest
);
if (!isNever(narrowedArgType)) {
return subjectSubtypeUnexpanded;
}
}
// We've completely eliminated the type based on the arguments.
return undefined;
}
);
}
if (!TypeBase.isInstantiable(exprType) && !isNever(exprType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocAddendum.typeNotClass().format({ type: evaluator.printType(exprType) }),
pattern.d.className
);
return isPositiveTest ? UnknownType.create() : type;
} else if (isInstantiableClass(exprType)) {
if (ClassType.isProtocolClass(exprType) && !ClassType.isRuntimeCheckable(exprType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocAddendum.protocolRequiresRuntimeCheckable(),
pattern.d.className
);
return isPositiveTest ? UnknownType.create() : type;
} else if (ClassType.isTypedDictClass(exprType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.typedDictInClassPattern(),
pattern.d.className
);
return isPositiveTest ? UnknownType.create() : type;
}
}
return evaluator.mapSubtypesExpandTypeVars(
exprType,
/* options */ undefined,
(expandedSubtype, unexpandedSubtype) => {
if (isAnyOrUnknown(expandedSubtype)) {
return unexpandedSubtype;
}
if (isInstantiableClass(expandedSubtype)) {
const expandedSubtypeInstance = convertToInstance(expandedSubtype);
const isPatternMetaclass = isMetaclassInstance(expandedSubtypeInstance);
return evaluator.mapSubtypesExpandTypeVars(type, /* options */ undefined, (subjectSubtypeExpanded) => {
if (isAnyOrUnknown(subjectSubtypeExpanded)) {
if (isInstantiableClass(expandedSubtype) && ClassType.isBuiltIn(expandedSubtype, 'Callable')) {
// Convert to an unknown callable type.
const unknownCallable = FunctionType.createSynthesizedInstance(
'',
FunctionTypeFlags.GradualCallableForm
);
FunctionType.addDefaultParams(
unknownCallable,
/* useUnknown */ isUnknown(subjectSubtypeExpanded)
);
unknownCallable.shared.declaredReturnType = subjectSubtypeExpanded;
return unknownCallable;
}
return convertToInstance(unexpandedSubtype);
}
// Handle the case where the class pattern references type() or a subtype thereof
// and the subject type is a class itself.
if (isPatternMetaclass && isInstantiableClass(subjectSubtypeExpanded)) {
const metaclass = subjectSubtypeExpanded.shared.effectiveMetaclass ?? UnknownType.create();
if (
evaluator.assignType(expandedSubtype, metaclass) ||
evaluator.assignType(metaclass, expandedSubtype)
) {
return subjectSubtypeExpanded;
}
return undefined;
}
// Handle NoneType specially.
if (
isNoneInstance(subjectSubtypeExpanded) &&
isInstantiableClass(expandedSubtype) &&
ClassType.isBuiltIn(expandedSubtype, 'NoneType')
) {
return subjectSubtypeExpanded;
}
// Handle Callable specially.
if (isInstantiableClass(expandedSubtype) && ClassType.isBuiltIn(expandedSubtype, 'Callable')) {
const callableType = getUnknownTypeForCallable();
if (evaluator.assignType(callableType, subjectSubtypeExpanded)) {
return subjectSubtypeExpanded;
}
const subjObjType = convertToInstance(subjectSubtypeExpanded);
if (evaluator.assignType(subjObjType, callableType)) {
return callableType;
}
return undefined;
}
if (isClassInstance(subjectSubtypeExpanded)) {
let resultType: Type;
if (evaluator.assignType(ClassType.cloneAsInstance(expandedSubtype), subjectSubtypeExpanded)) {
resultType = subjectSubtypeExpanded;
} else if (
evaluator.assignType(subjectSubtypeExpanded, ClassType.cloneAsInstance(expandedSubtype))
) {
resultType = addConditionToType(
convertToInstance(unexpandedSubtype),
getTypeCondition(subjectSubtypeExpanded)
);
// Try to retain the type arguments for the pattern class type.
if (isInstantiableClass(unexpandedSubtype) && isClassInstance(subjectSubtypeExpanded)) {
if (
ClassType.isSpecialBuiltIn(unexpandedSubtype) ||
unexpandedSubtype.shared.typeParams.length > 0
) {
const constraints = new ConstraintTracker();
const unspecializedMatchType = ClassType.specialize(
unexpandedSubtype,
/* typeArgs */ undefined
);
const matchTypeInstance = ClassType.cloneAsInstance(unspecializedMatchType);
if (
addConstraintsForExpectedType(
evaluator,
matchTypeInstance,
subjectSubtypeExpanded,
constraints,
/* liveTypeVarScopes */ undefined,
/* usageOffset */ undefined
)
) {
resultType = evaluator.solveAndApplyConstraints(
matchTypeInstance,
constraints,
{
replaceUnsolved: {
scopeIds: getTypeVarScopeIds(unexpandedSubtype),
tupleClassType: evaluator.getTupleClassType(),
},
}
) as ClassType;
}
}
}
} else {
return undefined;
}
// For a generic class pattern whose type parameters are bounded
// (e.g. `class Thing[T: bool]`), the subject may carry no type arguments,
// leaving the parameters unsolved. Fall back to each parameter's bound -
// but only for the pattern class itself. When the subject narrowed to a
// proper subclass of the pattern class, its own type arguments must be
// preserved rather than rebuilt from the (widened) base.
if (
isClassInstance(resultType) &&
isInstantiableClass(unexpandedSubtype) &&
ClassType.isSameGenericClass(resultType, ClassType.cloneAsInstance(unexpandedSubtype)) &&
unexpandedSubtype.shared.typeParams.some(
(param) => isTypeVar(param) && param.shared.boundType
)
) {
resultType = specializeBoundedMatchTypeParams(
evaluator,
unexpandedSubtype,
resultType.priv.typeArgs,
getTypeCondition(subjectSubtypeExpanded)
);
}
// Are there any positional arguments? If so, try to get the mappings for
// these arguments by fetching the __match_args__ symbol from the class.
let positionalArgNames: string[] = [];
if (pattern.d.args.some((arg) => !arg.d.name)) {
positionalArgNames = getPositionalMatchArgNames(evaluator, expandedSubtype);
}
let isMatchValid = true;
pattern.d.args.forEach((arg, index) => {
// Narrow the arg pattern. It's possible that the actual type of the object
// being matched is a subtype of the resultType, so it might contain additional
// attributes that we don't know about.
const narrowedArgType = narrowTypeOfClassPatternArg(
evaluator,
arg,
index,
positionalArgNames,
resultType,
isPositiveTest
);
if (isNever(narrowedArgType)) {
isMatchValid = false;
}
});
if (isMatchValid) {
return resultType;
}
}
return undefined;
});
}
return undefined;
}
);
}
// Some built-in classes are treated as special cases for the class pattern
// if a positional argument is used.
function isClassSpecialCaseForClassPattern(classType: ClassType) {
if (classPatternSpecialCases.some((className) => classType.shared.fullName === className)) {
return true;
}
// If the class supplies its own `__match_args__`, it's not a special case.
const matchArgsMemberInfo = lookUpClassMember(classType, '__match_args__');
if (matchArgsMemberInfo) {
return false;
}
// If the class derives from a built-in class, it is considered a special case.
for (const mroClass of classType.shared.mro) {
if (isClass(mroClass) && classPatternSpecialCases.some((className) => mroClass.shared.fullName === className)) {
return true;
}
}
return false;
}
// Narrows the pattern provided for a class pattern argument.
function narrowTypeOfClassPatternArg(
evaluator: TypeEvaluator,
arg: PatternClassArgumentNode,
argIndex: number,
positionalArgNames: string[],
matchType: Type,
isPositiveTest: boolean
) {
let argName: string | undefined;
if (arg.d.name) {
argName = arg.d.name.d.value;
} else if (argIndex < positionalArgNames.length) {
argName = positionalArgNames[argIndex];
}
if (isAnyOrUnknown(matchType)) {
return matchType;
}
if (!isClass(matchType)) {
return UnknownType.create();
}
// According to PEP 634, some built-in types use themselves as the subject
// for the first positional argument to a class pattern. Although the PEP does
// state so explicitly, this is true of subclasses of these built-in classes
// if the subclass doesn't define its own __match_args__.
let useSelfForPattern = false;
let selfForPatternType = matchType;
if (!arg.d.name && isClass(matchType) && argIndex === 0) {
if (isClassSpecialCaseForClassPattern(matchType)) {
useSelfForPattern = true;
} else if (positionalArgNames.length === 0) {
matchType.shared.mro.forEach((mroClass) => {
if (isClass(mroClass) && isClassSpecialCaseForClassPattern(mroClass)) {
selfForPatternType = mroClass;
useSelfForPattern = true;
}
});
}
}
let argType: Type | undefined;
if (useSelfForPattern) {
argType = ClassType.cloneAsInstance(selfForPatternType);
} else {
if (argName) {
argType = evaluator.useSpeculativeMode(arg, () =>
// We need to apply a rather ugly cast here because PatternClassArgumentNode is
// not technically an ExpressionNode, but it is OK to use it in this context.
evaluator.getTypeOfBoundMember(
arg as any as ExpressionNode,
ClassType.cloneAsInstance(matchType),
argName!
)
)?.type;
}
if (!argType) {
if (!isPositiveTest) {
return matchType;
}
// If the class type in question is "final", we know that no additional
// attributes can be added by subtypes, so it's safe to eliminate this
// type entirely.
if (ClassType.isFinal(matchType)) {
return NeverType.createNever();
}
argType = UnknownType.create();
}
}
return narrowTypeBasedOnPattern(evaluator, argType, arg.d.pattern, isPositiveTest);
}
function narrowTypeBasedOnValuePattern(
evaluator: TypeEvaluator,
subjectType: Type,
pattern: PatternValueNode,
isPositiveTest: boolean
): Type {
const valueType = evaluator.getTypeOfExpression(pattern.d.expr).type;
const narrowedSubtypes: Type[] = [];
evaluator.mapSubtypesExpandTypeVars(
valueType,
/* options */ undefined,
(valueSubtypeExpanded, valueSubtypeUnexpanded) => {
narrowedSubtypes.push(
evaluator.mapSubtypesExpandTypeVars(
subjectType,
{ conditionFilter: getTypeCondition(valueSubtypeExpanded) },
(subjectSubtypeExpanded) => {
// If this is a negative test, see if it's an enum value.
if (!isPositiveTest) {
if (
isClassInstance(subjectSubtypeExpanded) &&
isClassInstance(valueSubtypeExpanded) &&
isSameWithoutLiteralValue(subjectSubtypeExpanded, valueSubtypeExpanded)
) {
if (!isLiteralType(subjectSubtypeExpanded) && isLiteralType(valueSubtypeExpanded)) {
const expandedLiterals = enumerateLiteralsForType(
evaluator,
subjectSubtypeExpanded
);
if (expandedLiterals) {
return combineTypes(
expandedLiterals.filter(
(enumType) =>
!ClassType.isLiteralValueSame(valueSubtypeExpanded, enumType)
)
);
}
}
if (
isLiteralType(subjectSubtypeExpanded) &&
ClassType.isLiteralValueSame(valueSubtypeExpanded, subjectSubtypeExpanded)
) {
return undefined;
}
}
return subjectSubtypeExpanded;
}
if (isNever(valueSubtypeExpanded) || isNever(subjectSubtypeExpanded)) {
return NeverType.createNever();
}
if (isAnyOrUnknown(valueSubtypeExpanded) || isAnyOrUnknown(subjectSubtypeExpanded)) {
// If either type is "Unknown" (versus Any), propagate the Unknown.
return isUnknown(valueSubtypeExpanded) || isUnknown(subjectSubtypeExpanded)
? preserveUnknown(valueSubtypeExpanded, subjectSubtypeExpanded)
: AnyType.create();
}
// If both types are literals, we can compare the literal values directly.
if (
isClassInstance(subjectSubtypeExpanded) &&
isLiteralType(subjectSubtypeExpanded) &&
isClassInstance(valueSubtypeExpanded) &&
isLiteralType(valueSubtypeExpanded)
) {
return isSameWithoutLiteralValue(subjectSubtypeExpanded, valueSubtypeExpanded) &&
ClassType.isLiteralValueSame(valueSubtypeExpanded, subjectSubtypeExpanded)
? valueSubtypeUnexpanded
: undefined;
}
// Determine if assignment is supported for this combination of
// value subtype and matching subtype.
const returnType = evaluator.useSpeculativeMode(pattern.d.expr, () =>
evaluator.getTypeOfMagicMethodCall(
valueSubtypeExpanded,
'__eq__',
[{ type: subjectSubtypeExpanded }],
pattern.d.expr,
/* expectedType */ undefined
)
);
return returnType ? valueSubtypeUnexpanded : undefined;
}
)
);
return undefined;
}
);
return combineTypes(narrowedSubtypes);
}
// Returns information about all subtypes that match the definition of a "mapping" as
// specified in PEP 634.
function getMappingPatternInfo(evaluator: TypeEvaluator, type: Type, node: PatternAtomNode): MappingPatternInfo[] {
const mappingInfo: MappingPatternInfo[] = [];
doForEachSubtype(type, (subtype) => {
const concreteSubtype = evaluator.makeTopLevelTypeVarsConcrete(subtype);
if (isAnyOrUnknown(concreteSubtype)) {
mappingInfo.push({
subtype,
isDefinitelyMapping: false,
isDefinitelyNotMapping: false,
dictTypeArgs: {
key: concreteSubtype,
value: concreteSubtype,
},
});
return;
}
if (isClassInstance(concreteSubtype)) {
// Is it a TypedDict?
if (ClassType.isTypedDictClass(concreteSubtype)) {
mappingInfo.push({
subtype,
isDefinitelyMapping: true,
isDefinitelyNotMapping: false,
typedDict: concreteSubtype,
});
return;
}
const mappingType = evaluator.getTypingType(node, 'Mapping');
if (!mappingType || !isInstantiableClass(mappingType)) {
return;
}
const mappingObject = ClassType.cloneAsInstance(mappingType);
// Is it a subtype of Mapping?
const constraints = new ConstraintTracker();
if (evaluator.assignType(mappingObject, subtype, /* diag */ undefined, constraints)) {
const specializedMapping = evaluator.solveAndApplyConstraints(mappingObject, constraints) as ClassType;
if (specializedMapping.priv.typeArgs && specializedMapping.priv.typeArgs.length >= 2) {
mappingInfo.push({
subtype,
isDefinitelyMapping: true,
isDefinitelyNotMapping: false,
dictTypeArgs: {
key: specializedMapping.priv.typeArgs[0],
value: specializedMapping.priv.typeArgs[1],
},
});
}
return;
}
// Is it a superclass of Mapping?
if (evaluator.assignType(subtype, mappingObject)) {
mappingInfo.push({
subtype,
isDefinitelyMapping: false,
isDefinitelyNotMapping: false,
dictTypeArgs: {
key: UnknownType.create(),
value: UnknownType.create(),
},
});
return;
}
mappingInfo.push({
subtype,
isDefinitelyMapping: false,
isDefinitelyNotMapping: true,
});
}
});
return mappingInfo;
}
// Returns information about all subtypes that match the definition of a "sequence" as
// specified in PEP 634. For types that are not sequences or sequences that are not of
// sufficient length, it sets definiteNoMatch to true.
function getSequencePatternInfo(
evaluator: TypeEvaluator,
pattern: PatternSequenceNode,
type: Type
): SequencePatternInfo[] {
const patternEntryCount = pattern.d.entries.length;
const patternStarEntryIndex = pattern.d.starEntryIndex;
const sequenceInfo: SequencePatternInfo[] = [];
doForEachSubtype(type, (subtype) => {
const concreteSubtype = evaluator.makeTopLevelTypeVarsConcrete(subtype);
let mroClassToSpecialize: ClassType | undefined;
if (isClassInstance(concreteSubtype)) {
for (const mroClass of concreteSubtype.shared.mro) {
if (!isInstantiableClass(mroClass)) {
break;
}
// Strings, bytes, and bytearray are explicitly excluded.
if (
ClassType.isBuiltIn(mroClass, 'str') ||
ClassType.isBuiltIn(mroClass, 'bytes') ||
ClassType.isBuiltIn(mroClass, 'bytearray')
) {
// This is definitely not a match.
sequenceInfo.push({
subtype,
entryTypes: [],
isIndeterminateLength: true,
isDefiniteNoMatch: true,
});
return;
}
if (ClassType.isBuiltIn(mroClass, 'Sequence')) {
mroClassToSpecialize = mroClass;
break;
}
if (isTupleClass(mroClass)) {
mroClassToSpecialize = mroClass;
break;
}
}
if (mroClassToSpecialize) {
const specializedSequence = partiallySpecializeType(
mroClassToSpecialize,
concreteSubtype,
evaluator.getTypeClassType()
) as ClassType;
if (isTupleClass(specializedSequence)) {
const typeArgs = specializedSequence.priv.tupleTypeArgs ?? [
{ type: UnknownType.create(), isUnbounded: true },
];
let tupleIndeterminateIndex = typeArgs.findIndex(
(t) => t.isUnbounded || isUnpackedTypeVarTuple(t.type) || isUnpackedTypeVar(t.type)
);
let tupleDeterminateEntryCount = typeArgs.length;
// If the tuple contains an indeterminate entry, expand or remove that
// entry to match the length of the pattern if possible.
let expandedIndeterminate = false;
// Tracks whether the indeterminate entry was spliced out to contract the tuple
// to fit a shorter pattern. This preserves "potential match" semantics after
// the splice resets tupleIndeterminateIndex to -1.
let removedIndeterminate = false;
if (tupleIndeterminateIndex >= 0) {
tupleDeterminateEntryCount--;
while (typeArgs.length < patternEntryCount) {
typeArgs.splice(tupleIndeterminateIndex, 0, typeArgs[tupleIndeterminateIndex]);
tupleDeterminateEntryCount++;
tupleIndeterminateIndex++;
expandedIndeterminate = true;
}
if (typeArgs.length > patternEntryCount && patternStarEntryIndex === undefined) {
typeArgs.splice(tupleIndeterminateIndex, 1);
removedIndeterminate = true;
tupleIndeterminateIndex = -1;
removedIndeterminate = true;
}
}
// If the pattern contains a star entry and there are too many entries
// in the tuple, we can collapse some of them into the star entry.
if (
patternStarEntryIndex !== undefined &&
typeArgs.length >= 2 &&
typeArgs.length > patternEntryCount
) {
const entriesToCombine = typeArgs.length - patternEntryCount + 1;
const removedEntries = typeArgs.splice(patternStarEntryIndex, entriesToCombine);
typeArgs.splice(patternStarEntryIndex, 0, {
type: combineTypes(removedEntries.map((t) => t.type)),
isUnbounded: removedEntries.every(
(t) => t.isUnbounded || isUnpackedTypeVarTuple(t.type) || isUnpackedTypeVar(t.type)
),
});
tupleDeterminateEntryCount -= entriesToCombine;
if (!typeArgs[patternStarEntryIndex].isUnbounded) {
tupleDeterminateEntryCount++;
}
// If the collapsed range included the tupleIndeterminateIndex, adjust
// it to reflect the new collapsed entry.
if (
tupleIndeterminateIndex >= patternStarEntryIndex &&
tupleIndeterminateIndex < patternStarEntryIndex + entriesToCombine
) {
tupleIndeterminateIndex = patternStarEntryIndex;
}
}
if (typeArgs.length === patternEntryCount) {
let isDefiniteNoMatch = false;
let isPotentialNoMatch = tupleIndeterminateIndex >= 0 || removedIndeterminate;
// If we removed an unbounded entry to make the lengths match,
// this is a potential match (not definite) because the original
// tuple could have different lengths.
if (removedIndeterminate) {
isPotentialNoMatch = true;
}
// If the pattern includes a "star entry" and the tuple includes an
// indeterminate-length entry that aligns to the star entry, we can
// assume it will always match.
if (
!expandedIndeterminate &&
patternStarEntryIndex !== undefined &&
tupleIndeterminateIndex >= 0 &&
pattern.d.entries.length - 1 === tupleDeterminateEntryCount &&
patternStarEntryIndex === tupleIndeterminateIndex
) {
isPotentialNoMatch = false;
}
for (let i = 0; i < patternEntryCount; i++) {
const subPattern = pattern.d.entries[i];
const typeArg = typeArgs[i].type;
const narrowedType = narrowTypeBasedOnPattern(
evaluator,
typeArg,
subPattern,
/* isPositiveTest */ true
);
if (isNever(narrowedType)) {
isDefiniteNoMatch = true;
}
}
sequenceInfo.push({
subtype,
entryTypes: isDefiniteNoMatch ? [] : typeArgs.map((t) => t.type),
isIndeterminateLength: false,
isTuple: true,
isUnboundedTuple: removedIndeterminate || tupleIndeterminateIndex >= 0,
isDefiniteNoMatch,
isPotentialNoMatch,
});
return;
}
// If the pattern contains a star entry and the pattern associated with
// the star entry is unbounded, we can remove it completely under the
// assumption that the star pattern will capture nothing.
if (patternStarEntryIndex !== undefined) {
let tryMatchStarSequence = false;
if (typeArgs.length === patternEntryCount - 1) {
tryMatchStarSequence = true;
typeArgs.splice(patternStarEntryIndex, 0, {
type: AnyType.create(),
isUnbounded: true,
});
} else if (
typeArgs.length === patternEntryCount &&
typeArgs[patternStarEntryIndex].isUnbounded
) {
tryMatchStarSequence = true;
}
if (tryMatchStarSequence) {
let isDefiniteNoMatch = false;
for (let i = 0; i < patternEntryCount; i++) {
if (i === patternStarEntryIndex) {
continue;
}
const subPattern = pattern.d.entries[i];
const typeArg = typeArgs[i].type;
const narrowedType = narrowTypeBasedOnPattern(
evaluator,
typeArg,
subPattern,
/* isPositiveTest */ true
);
if (isNever(narrowedType)) {
isDefiniteNoMatch = true;
}
}
sequenceInfo.push({
subtype,
entryTypes: isDefiniteNoMatch ? [] : typeArgs.map((t) => t.type),
isIndeterminateLength: false,
isTuple: true,
isUnboundedTuple: tupleIndeterminateIndex >= 0,
isDefiniteNoMatch,
});
return;
}
}
} else {
sequenceInfo.push({
subtype,
entryTypes: [
specializedSequence.priv.typeArgs && specializedSequence.priv.typeArgs.length > 0
? specializedSequence.priv.typeArgs[0]
: UnknownType.create(),
],
isIndeterminateLength: true,
isDefiniteNoMatch: false,
});
return;
}
}
}
if (!mroClassToSpecialize) {
const sequenceType = evaluator.getTypingType(pattern, 'Sequence');
if (sequenceType && isInstantiableClass(sequenceType)) {
const sequenceObject = ClassType.cloneAsInstance(sequenceType);
// Is it a subtype of Sequence?
const constraints = new ConstraintTracker();
if (evaluator.assignType(sequenceObject, subtype, /* diag */ undefined, constraints)) {
const specializedSequence = evaluator.solveAndApplyConstraints(
sequenceObject,
constraints
) as ClassType;
if (specializedSequence.priv.typeArgs && specializedSequence.priv.typeArgs.length > 0) {
sequenceInfo.push({
subtype,
entryTypes: [specializedSequence.priv.typeArgs[0]],
isIndeterminateLength: true,
isDefiniteNoMatch: false,
isPotentialNoMatch: false,
});
return;
}
}
// If it wasn't a subtype of Sequence, see if it's a supertype.
const sequenceConstraints = new ConstraintTracker();
if (
addConstraintsForExpectedType(
evaluator,
ClassType.cloneAsInstance(sequenceType),
subtype,
sequenceConstraints,
getTypeVarScopesForNode(pattern),
pattern.start
)
) {
const specializedSequence = evaluator.solveAndApplyConstraints(
ClassType.cloneAsInstantiable(sequenceType),
sequenceConstraints
) as ClassType;
if (specializedSequence.priv.typeArgs && specializedSequence.priv.typeArgs.length > 0) {
sequenceInfo.push({
subtype,
entryTypes: [specializedSequence.priv.typeArgs[0]],
isIndeterminateLength: true,
isDefiniteNoMatch: false,
isPotentialNoMatch: true,
});
return;
}
}
if (
evaluator.assignType(
subtype,
ClassType.specialize(ClassType.cloneAsInstance(sequenceType), [UnknownType.create()])
)
) {
sequenceInfo.push({
subtype,
entryTypes: [UnknownType.create()],
isIndeterminateLength: true,
isDefiniteNoMatch: false,
isPotentialNoMatch: true,
});
return;
}
}
}
// Push an entry that indicates that this is definitely not a match.
sequenceInfo.push({
subtype,
entryTypes: [],
isIndeterminateLength: true,
isDefiniteNoMatch: true,
});
});
return sequenceInfo;
}
function getTypeOfPatternSequenceEntry(
evaluator: TypeEvaluator,
node: ParseNode,
sequenceInfo: SequencePatternInfo,
entryIndex: number,
entryCount: number,
starEntryIndex: number | undefined,
unpackStarEntry: boolean
): Type {
if (sequenceInfo.isIndeterminateLength) {
let entryType = sequenceInfo.entryTypes[0];
if (!unpackStarEntry && entryIndex === starEntryIndex && !isNever(entryType)) {
entryType = wrapTypeInList(evaluator, node, entryType);
}
return entryType;
}
if (starEntryIndex === undefined || entryIndex < starEntryIndex) {
return sequenceInfo.entryTypes[entryIndex];
}
if (entryIndex === starEntryIndex) {
// Create a list out of the entries that map to the star entry.
// Note that we strip literal types here.
const starEntryTypes = sequenceInfo.entryTypes
.slice(starEntryIndex, starEntryIndex + sequenceInfo.entryTypes.length - entryCount + 1)
.map((type) => {
// If this is a TypeVarTuple, there's not much we can say about
// its type other than it's "Unknown". We could evaluate it as an
// "object", but that will cause problems given that this type will
// be wrapped in a "list" below, and lists are invariant.
if (isTypeVarTuple(type) && !type.priv.isInUnion) {
return UnknownType.create();
}
return evaluator.stripLiteralValue(type);
});
let entryType = combineTypes(starEntryTypes);
if (!unpackStarEntry) {
entryType = wrapTypeInList(evaluator, node, entryType);
}
return entryType;
}
// The entry index is past the index of the star entry, so we need
// to index from the end of the sequence rather than the start.
const itemIndex = sequenceInfo.entryTypes.length - (entryCount - entryIndex);
assert(itemIndex >= 0 && itemIndex < sequenceInfo.entryTypes.length);
return sequenceInfo.entryTypes[itemIndex];
}
// Recursively assigns the specified type to the pattern and any capture
// nodes within it. It returns the narrowed type, as dictated by the pattern.
export function assignTypeToPatternTargets(
evaluator: TypeEvaluator,
type: Type,
isTypeIncomplete: boolean,
pattern: PatternAtomNode
): Type {
// Further narrow the type based on this pattern.
const narrowedType = narrowTypeBasedOnPattern(evaluator, type, pattern, /* positiveTest */ true);
switch (pattern.nodeType) {
case ParseNodeType.PatternSequence: {
const sequenceInfo = getSequencePatternInfo(evaluator, pattern, narrowedType).filter(
(seqInfo) => !seqInfo.isDefiniteNoMatch
);
pattern.d.entries.forEach((entry, index) => {
const entryType = combineTypes(
sequenceInfo.map((info) =>
getTypeOfPatternSequenceEntry(
evaluator,
pattern,
info,
index,
pattern.d.entries.length,
pattern.d.starEntryIndex,
/* unpackStarEntry */ false
)
)
);
assignTypeToPatternTargets(evaluator, entryType, isTypeIncomplete, entry);
});
break;
}
case ParseNodeType.PatternAs: {
if (pattern.d.target) {
evaluator.assignTypeToExpression(
pattern.d.target,
{ type: narrowedType, isIncomplete: isTypeIncomplete },
pattern.d.target
);
}
let runningNarrowedType = narrowedType;
pattern.d.orPatterns.forEach((orPattern) => {
assignTypeToPatternTargets(evaluator, runningNarrowedType, isTypeIncomplete, orPattern);
// OR patterns are evaluated left to right, so we can narrow
// the type as we go.
runningNarrowedType = narrowTypeBasedOnPattern(
evaluator,
runningNarrowedType,
orPattern,
/* positiveTest */ false
);
});
break;
}
case ParseNodeType.PatternCapture: {
if (pattern.d.isWildcard) {
if (!isTypeIncomplete) {
if (isUnknown(narrowedType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportUnknownVariableType,
LocMessage.wildcardPatternTypeUnknown(),
pattern.d.target
);
} else if (isPartlyUnknown(narrowedType)) {
const diagAddendum = new DiagnosticAddendum();
diagAddendum.addMessage(
LocAddendum.typeOfSymbol().format({
name: '_',
type: evaluator.printType(narrowedType, { expandTypeAlias: true }),
})
);
evaluator.addDiagnostic(
DiagnosticRule.reportUnknownVariableType,
LocMessage.wildcardPatternTypePartiallyUnknown() + diagAddendum.getString(),
pattern.d.target
);
}
}
} else {
evaluator.assignTypeToExpression(
pattern.d.target,
{ type: narrowedType, isIncomplete: isTypeIncomplete },
pattern.d.target
);
}
break;
}
case ParseNodeType.PatternMapping: {
const mappingInfo = getMappingPatternInfo(evaluator, narrowedType, pattern);
pattern.d.entries.forEach((mappingEntry) => {
const keyTypes: Type[] = [];
const valueTypes: Type[] = [];
mappingInfo.forEach((mappingSubtypeInfo) => {
if (mappingSubtypeInfo.typedDict) {
if (mappingEntry.nodeType === ParseNodeType.PatternMappingKeyEntry) {
const keyType = narrowTypeBasedOnPattern(
evaluator,
evaluator.getBuiltInObject(pattern, 'str'),
mappingEntry.d.keyPattern,
/* isPositiveTest */ true
);
keyTypes.push(keyType);
doForEachSubtype(keyType, (keySubtype) => {
if (
isClassInstance(keySubtype) &&
ClassType.isBuiltIn(keySubtype, 'str') &&
isLiteralType(keySubtype)
) {
const tdEntries = getTypedDictMembersForClass(
evaluator,
mappingSubtypeInfo.typedDict!
);
const valueInfo = tdEntries.knownItems.get(keySubtype.priv.literalValue as string);
valueTypes.push(valueInfo ? valueInfo.valueType : UnknownType.create());
} else {
valueTypes.push(UnknownType.create());
}
});
} else if (mappingEntry.nodeType === ParseNodeType.PatternMappingExpandEntry) {
keyTypes.push(evaluator.getBuiltInObject(pattern, 'str'));
valueTypes.push(evaluator.getObjectType());
}
} else if (mappingSubtypeInfo.dictTypeArgs) {
if (mappingEntry.nodeType === ParseNodeType.PatternMappingKeyEntry) {
const keyType = narrowTypeBasedOnPattern(
evaluator,
mappingSubtypeInfo.dictTypeArgs.key,
mappingEntry.d.keyPattern,
/* isPositiveTest */ true
);
keyTypes.push(keyType);
valueTypes.push(
narrowTypeBasedOnPattern(
evaluator,
mappingSubtypeInfo.dictTypeArgs.value,
mappingEntry.d.valuePattern,
/* isPositiveTest */ true
)
);
} else if (mappingEntry.nodeType === ParseNodeType.PatternMappingExpandEntry) {
keyTypes.push(mappingSubtypeInfo.dictTypeArgs.key);
valueTypes.push(mappingSubtypeInfo.dictTypeArgs.value);
}
}
});
const keyType = combineTypes(keyTypes);
const valueType = combineTypes(valueTypes);
if (mappingEntry.nodeType === ParseNodeType.PatternMappingKeyEntry) {
assignTypeToPatternTargets(evaluator, keyType, isTypeIncomplete, mappingEntry.d.keyPattern);
assignTypeToPatternTargets(evaluator, valueType, isTypeIncomplete, mappingEntry.d.valuePattern);
} else if (mappingEntry.nodeType === ParseNodeType.PatternMappingExpandEntry) {
const dictClass = evaluator.getBuiltInType(pattern, 'dict');
const strType = evaluator.getBuiltInObject(pattern, 'str');
const dictType =
dictClass && isInstantiableClass(dictClass) && isClassInstance(strType)
? ClassType.cloneAsInstance(ClassType.specialize(dictClass, [keyType, valueType]))
: UnknownType.create();
evaluator.assignTypeToExpression(
mappingEntry.d.target,
{ type: dictType, isIncomplete: isTypeIncomplete },
mappingEntry.d.target
);
}
});
break;
}
case ParseNodeType.PatternClass: {
const argTypes: Type[][] = pattern.d.args.map((arg) => []);
evaluator.mapSubtypesExpandTypeVars(narrowedType, /* options */ undefined, (expandedSubtype) => {
if (isClassInstance(expandedSubtype)) {
doForEachSubtype(narrowedType, (subjectSubtype) => {
const concreteSubtype = evaluator.makeTopLevelTypeVarsConcrete(subjectSubtype);
if (isAnyOrUnknown(concreteSubtype)) {
pattern.d.args.forEach((arg, index) => {
argTypes[index].push(concreteSubtype);
});
} else if (isClassInstance(concreteSubtype)) {
// Are there any positional arguments? If so, try to get the mappings for
// these arguments by fetching the __match_args__ symbol from the class.
let positionalArgNames: string[] = [];
if (pattern.d.args.some((arg) => !arg.d.name)) {
positionalArgNames = getPositionalMatchArgNames(
evaluator,
ClassType.cloneAsInstantiable(expandedSubtype)
);
}
pattern.d.args.forEach((arg, index) => {
const narrowedArgType = narrowTypeOfClassPatternArg(
evaluator,
arg,
index,
positionalArgNames,
ClassType.cloneAsInstantiable(expandedSubtype),
/* isPositiveTest */ true
);
argTypes[index].push(narrowedArgType);
});
}
});
} else {
pattern.d.args.forEach((arg, index) => {
argTypes[index].push(UnknownType.create());
});
}
return undefined;
});
pattern.d.args.forEach((arg, index) => {
assignTypeToPatternTargets(evaluator, combineTypes(argTypes[index]), isTypeIncomplete, arg.d.pattern);
});
break;
}
case ParseNodeType.PatternLiteral:
case ParseNodeType.PatternValue:
case ParseNodeType.Error: {
// Nothing to do here.
break;
}
}
return narrowedType;
}
function wrapTypeInList(evaluator: TypeEvaluator, node: ParseNode, type: Type): Type {
if (isNever(type)) {
return type;
}
const listObjectType = convertToInstance(evaluator.getBuiltInObject(node, 'list'));
if (listObjectType && isClassInstance(listObjectType)) {
// If the type is a union that contains an Any or Unknown, eliminate the other
// types before wrapping it in a list.
type = containsAnyOrUnknown(type, /* recurse */ false) ?? type;
return ClassType.specialize(listObjectType, [type]);
}
return UnknownType.create();
}
export function validateClassPattern(evaluator: TypeEvaluator, pattern: PatternClassNode) {
let exprType = evaluator.getTypeOfExpression(pattern.d.className, EvalFlags.CallBaseDefaults).type;
// If the expression is a type alias or other special form, treat it
// as the special form rather than the class.
if (exprType.props?.specialForm) {
exprType = exprType.props.specialForm;
}
if (isAnyOrUnknown(exprType)) {
return;
}
// Check for certain uses of type aliases that generate runtime exceptions.
if (
exprType.props?.typeAliasInfo &&
isInstantiableClass(exprType) &&
exprType.priv.typeArgs &&
exprType.priv.isTypeArgExplicit
) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.classPatternTypeAlias().format({ type: evaluator.printType(exprType) }),
pattern.d.className
);
} else if (!isInstantiableClass(exprType)) {
if (!isNever(exprType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocAddendum.typeNotClass().format({ type: evaluator.printType(exprType) }),
pattern.d.className
);
}
} else if (ClassType.isNewTypeClass(exprType)) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.classPatternNewType().format({ type: evaluator.printType(exprType) }),
pattern.d.className
);
} else {
const isBuiltIn = isClassSpecialCaseForClassPattern(exprType);
// If it's a special-case builtin class, only positional arguments are allowed.
if (isBuiltIn) {
if (pattern.d.args.length === 1 && pattern.d.args[0].d.name) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.classPatternBuiltInArgPositional(),
pattern.d.args[0].d.name
);
}
}
// Emits an error if the supplied number of positional patterns is less than
// expected for the given subject type.
let positionalPatternCount = pattern.d.args.findIndex((arg) => arg.d.name !== undefined);
if (positionalPatternCount < 0) {
positionalPatternCount = pattern.d.args.length;
}
let expectedPatternCount = 1;
if (!isBuiltIn) {
let positionalArgNames: string[] = [];
if (pattern.d.args.some((arg) => !arg.d.name)) {
positionalArgNames = getPositionalMatchArgNames(evaluator, exprType);
}
expectedPatternCount = positionalArgNames.length;
}
if (positionalPatternCount > expectedPatternCount) {
evaluator.addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.classPatternPositionalArgCount().format({
type: exprType.shared.name,
expected: expectedPatternCount,
received: positionalPatternCount,
}),
pattern.d.args[expectedPatternCount]
);
}
}
}
// Determines whether the reference expression has a relationship to the subject expression
// in such a way that the type of the reference expression can be narrowed based
// on the narrowed type of the subject expression.
export function getPatternSubtypeNarrowingCallback(
evaluator: TypeEvaluator,
reference: CodeFlowReferenceExpressionNode,
subjectExpression: ExpressionNode
): PatternSubtypeNarrowingCallback | undefined {
// Look for a subject expression of the form <reference>[<literal>] where
// <literal> is either a str (for TypedDict discrimination) or an int
// (for tuple discrimination).
if (
subjectExpression.nodeType === ParseNodeType.Index &&
subjectExpression.d.items.length === 1 &&
!subjectExpression.d.trailingComma &&
subjectExpression.d.items[0].d.argCategory === ArgCategory.Simple &&
isMatchingExpression(reference, subjectExpression.d.leftExpr)
) {
const indexTypeResult = evaluator.getTypeOfExpression(subjectExpression.d.items[0].d.valueExpr);
const indexType = indexTypeResult.type;
if (isClassInstance(indexType) && isLiteralType(indexType)) {
if (ClassType.isBuiltIn(indexType, ['int', 'str'])) {
const unnarrowedReferenceTypeResult = evaluator.getTypeOfExpression(
subjectExpression.d.leftExpr,
EvalFlags.CallBaseDefaults
);
const unnarrowedReferenceType = unnarrowedReferenceTypeResult.type;
return (narrowedSubjectType: Type) => {
let canNarrow = true;
const typesToCombine: Type[] = [];
doForEachSubtype(narrowedSubjectType, (subtype) => {
subtype = evaluator.makeTopLevelTypeVarsConcrete(subtype);
if (isClassInstance(subtype) && subtype.priv.literalValue !== undefined) {
if (ClassType.isBuiltIn(indexType, 'str')) {
typesToCombine.push(
narrowTypeForDiscriminatedDictEntryComparison(
evaluator,
unnarrowedReferenceType,
indexType,
subtype,
/* isPositiveTest */ true
)
);
} else {
typesToCombine.push(
narrowTypeForDiscriminatedTupleComparison(
evaluator,
unnarrowedReferenceType,
indexType,
subtype,
/* isPositiveTest */ true
)
);
}
} else if (!isNever(subtype)) {
// We don't know how to narrow in this case.
canNarrow = false;
}
});
if (!canNarrow) {
return undefined;
}
return {
type: combineTypes(typesToCombine),
isIncomplete: indexTypeResult.isIncomplete || unnarrowedReferenceTypeResult.isIncomplete,
};
};
}
}
}
// Look for a subject expression that contains the reference
// expression as an entry in a tuple.
if (subjectExpression.nodeType === ParseNodeType.Tuple) {
const matchingEntryIndex = subjectExpression.d.items.findIndex((expr) => isMatchingExpression(reference, expr));
if (matchingEntryIndex >= 0) {
const typeResult = evaluator.getTypeOfExpression(subjectExpression.d.items[matchingEntryIndex]);
return (narrowedSubjectType: Type) => {
let canNarrow = true;
const narrowedSubtypes: Type[] = [];
doForEachSubtype(narrowedSubjectType, (subtype) => {
if (
isClassInstance(subtype) &&
ClassType.isBuiltIn(subtype, 'tuple') &&
subtype.priv.tupleTypeArgs &&
matchingEntryIndex < subtype.priv.tupleTypeArgs.length &&
subtype.priv.tupleTypeArgs.every((e) => !e.isUnbounded)
) {
narrowedSubtypes.push(subtype.priv.tupleTypeArgs[matchingEntryIndex].type);
} else if (isNever(narrowedSubjectType)) {
narrowedSubtypes.push(narrowedSubjectType);
} else {
canNarrow = false;
}
});
return canNarrow
? { type: combineTypes(narrowedSubtypes), isIncomplete: typeResult.isIncomplete }
: undefined;
};
}
}
// Look for a subject expression of the form "a.b" where "b" is an attribute
// that is annotated with a literal type.
if (
subjectExpression.nodeType === ParseNodeType.MemberAccess &&
isMatchingExpression(reference, subjectExpression.d.leftExpr)
) {
const unnarrowedReferenceTypeResult = evaluator.getTypeOfExpression(
subjectExpression.d.leftExpr,
EvalFlags.CallBaseDefaults
);
const unnarrowedReferenceType = unnarrowedReferenceTypeResult.type;
return (narrowedSubjectType: Type) => {
if (isNever(narrowedSubjectType)) {
return { type: NeverType.createNever() };
}
if (!isLiteralTypeOrUnion(narrowedSubjectType)) {
return undefined;
}
const resultType = mapSubtypes(narrowedSubjectType, (literalSubtype) => {
assert(isClassInstance(literalSubtype) && literalSubtype.priv.literalValue !== undefined);
return narrowTypeForDiscriminatedLiteralFieldComparison(
evaluator,
unnarrowedReferenceType,
subjectExpression.d.member.d.value,
literalSubtype,
/* isPositiveTest */ true
);
});
return {
type: resultType,
};
};
}
return undefined;
}
function reportUnnecessaryPattern(evaluator: TypeEvaluator, pattern: PatternAtomNode, subjectType: Type): void {
// If this is a simple wildcard pattern, exempt it from this diagnostic.
if (
pattern.nodeType === ParseNodeType.PatternAs &&
pattern.d.orPatterns.length === 1 &&
pattern.d.orPatterns[0].nodeType === ParseNodeType.PatternCapture &&
pattern.d.orPatterns[0].d.isWildcard
) {
return;
}
evaluator.addDiagnostic(
DiagnosticRule.reportUnnecessaryComparison,
LocMessage.patternNeverMatches().format({ type: evaluator.printType(subjectType) }),
pattern
);
}