mirror of
https://github.com/microsoft/pyright.git
synced 2024-08-17 03:40:44 +03:00
Added smarter handling of empty lists ([]
) and dicts ({}
). Previously, these were inferred to have types list[Unknown]
and dict[Unknown, Unknown]
, respectively. They are now provided with a known type if the variable is assigned a known list or dict type along another code path.
This commit is contained in:
parent
ca2e35d217
commit
e5714b3365
@ -107,6 +107,20 @@ var5 = (3,) # Type is assumed to be Tuple[int]
|
||||
var6: Tuple[float, ...] = (3,) # Type of RHS is now Tuple[float, ...]
|
||||
```
|
||||
|
||||
### Empty List and Dictionary Type Inference
|
||||
|
||||
It is common to initialize a local variable or instance variable to an empty list (`[]`) or empty dictionary (`{}`) on one code path but initialize it to a non-empty list or dictionary on other code paths. In such cases, Pyright will infer the type based on the non-empty list or dictionary and suppress errors about a “partially unknown type”.
|
||||
|
||||
```python
|
||||
if some_condition:
|
||||
my_list = []
|
||||
else:
|
||||
my_list = ["a", "b"]
|
||||
|
||||
reveal_type(my_list) # list[str]
|
||||
```
|
||||
|
||||
|
||||
### Return Type Inference
|
||||
|
||||
As with variable assignments, function return types can be inferred from the `return` statements found within that function. The returned type is assumed to be the union of all types returned from all `return` statements. If a `return` statement is not followed by an expression, it is assumed to return `None`. Likewise, if the function does not end in a `return` statement, and the end of the function is reachable, an implicit `return None` is assumed.
|
||||
@ -221,7 +235,7 @@ def func1(a: int):
|
||||
|
||||
When inferring the type of a list expression (in the absence of bidirectional inference hints), Pyright uses the following heuristics:
|
||||
|
||||
1. If the list is empty (`[]`), assume `List[Unknown]`.
|
||||
1. If the list is empty (`[]`), assume `List[Unknown]` (unless a known list type is assigned to the same variable along another code path).
|
||||
2. If the list contains at least one element and all elements are the same type T, infer the type `List[T]`.
|
||||
3. If the list contains multiple elements that are of different types, the behavior depends on the `strictListInference` configuration setting. By default this setting is off.
|
||||
|
||||
|
@ -1153,14 +1153,21 @@ export function createTypeEvaluator(
|
||||
node.leftExpression,
|
||||
typeResult.type,
|
||||
/* isTypeIncomplete */ false,
|
||||
node.rightExpression
|
||||
node.rightExpression,
|
||||
/* ignoreEmptyContainers */ true
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case ParseNodeType.AssignmentExpression: {
|
||||
typeResult = getTypeOfExpression(node.rightExpression);
|
||||
assignTypeToExpression(node.name, typeResult.type, /* isTypeIncomplete */ false, node.rightExpression);
|
||||
assignTypeToExpression(
|
||||
node.name,
|
||||
typeResult.type,
|
||||
/* isTypeIncomplete */ false,
|
||||
node.rightExpression,
|
||||
/* ignoreEmptyContainers */ true
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -2979,7 +2986,8 @@ export function createTypeEvaluator(
|
||||
DiagnosticRule.reportUnknownMemberType,
|
||||
node.memberName,
|
||||
srcType,
|
||||
node
|
||||
node,
|
||||
/* ignoreEmptyContainers */ true
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3076,7 +3084,7 @@ export function createTypeEvaluator(
|
||||
}
|
||||
}
|
||||
|
||||
assignTypeToExpression(expr, targetType, isTypeIncomplete, srcExpr);
|
||||
assignTypeToExpression(expr, targetType, isTypeIncomplete, srcExpr, /* ignoreEmptyContainers */ true);
|
||||
});
|
||||
|
||||
writeTypeCache(target, type, isTypeIncomplete);
|
||||
@ -3194,6 +3202,7 @@ export function createTypeEvaluator(
|
||||
type: Type,
|
||||
isTypeIncomplete: boolean,
|
||||
srcExpr: ExpressionNode,
|
||||
ignoreEmptyContainers = false,
|
||||
expectedTypeDiagAddendum?: DiagnosticAddendum
|
||||
) {
|
||||
// Is the source expression a TypeVar() call?
|
||||
@ -3235,7 +3244,8 @@ export function createTypeEvaluator(
|
||||
DiagnosticRule.reportUnknownVariableType,
|
||||
target,
|
||||
type,
|
||||
target
|
||||
target,
|
||||
ignoreEmptyContainers
|
||||
);
|
||||
}
|
||||
|
||||
@ -3304,6 +3314,7 @@ export function createTypeEvaluator(
|
||||
type,
|
||||
/* isIncomplete */ false,
|
||||
srcExpr,
|
||||
ignoreEmptyContainers,
|
||||
expectedTypeDiagAddendum
|
||||
);
|
||||
break;
|
||||
@ -3321,7 +3332,13 @@ export function createTypeEvaluator(
|
||||
const iteratedType = getTypeFromIterator(type, /* isAsync */ false, srcExpr) || UnknownType.create();
|
||||
|
||||
target.entries.forEach((entry) => {
|
||||
assignTypeToExpression(entry, iteratedType, /* isIncomplete */ false, srcExpr);
|
||||
assignTypeToExpression(
|
||||
entry,
|
||||
iteratedType,
|
||||
/* isIncomplete */ false,
|
||||
srcExpr,
|
||||
ignoreEmptyContainers
|
||||
);
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -4385,7 +4402,8 @@ export function createTypeEvaluator(
|
||||
DiagnosticRule.reportUnknownMemberType,
|
||||
node.memberName,
|
||||
type,
|
||||
node
|
||||
node,
|
||||
/* ignoreEmptyContainers */ false
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -9761,6 +9779,8 @@ export function createTypeEvaluator(
|
||||
let keyTypes: Type[] = [];
|
||||
let valueTypes: Type[] = [];
|
||||
|
||||
let isEmptyContainer = false;
|
||||
|
||||
// Infer the key and value types if possible.
|
||||
getKeyAndValueTypesFromDictionary(
|
||||
node,
|
||||
@ -9794,9 +9814,23 @@ export function createTypeEvaluator(
|
||||
}
|
||||
} else {
|
||||
valueType = expectedType ? AnyType.create() : UnknownType.create();
|
||||
isEmptyContainer = true;
|
||||
}
|
||||
|
||||
const type = getBuiltInObject(node, 'dict', [keyType, valueType]);
|
||||
const dictClass = getBuiltInType(node, 'dict');
|
||||
const type = isClass(dictClass)
|
||||
? ObjectType.create(
|
||||
ClassType.cloneForSpecialization(
|
||||
dictClass,
|
||||
[keyType, valueType],
|
||||
/* isTypeArgumentExplicit */ true,
|
||||
/* skipAbstractClassTest */ false,
|
||||
/* TupleTypeArguments */ undefined,
|
||||
isEmptyContainer
|
||||
)
|
||||
)
|
||||
: UnknownType.create();
|
||||
|
||||
return { type, node };
|
||||
}
|
||||
|
||||
@ -10012,6 +10046,8 @@ export function createTypeEvaluator(
|
||||
|
||||
// Attempts to infer the type of a list statement with no "expected type".
|
||||
function getTypeFromListInferred(node: ListNode, expectedType: Type | undefined): TypeResult {
|
||||
let isEmptyContainer = false;
|
||||
|
||||
// If we received an expected entry type that of "object",
|
||||
// allow Any rather than generating an "Unknown".
|
||||
let expectedEntryType: Type | undefined;
|
||||
@ -10050,9 +10086,24 @@ export function createTypeEvaluator(
|
||||
// Is the list homogeneous? If so, use stricter rules. Otherwise relax the rules.
|
||||
inferredEntryType = areTypesSame(entryTypes) ? entryTypes[0] : inferredEntryType;
|
||||
}
|
||||
} else {
|
||||
isEmptyContainer = true;
|
||||
}
|
||||
|
||||
const type = getBuiltInObject(node, 'list', [inferredEntryType]);
|
||||
const listClass = getBuiltInType(node, 'list');
|
||||
const type = isClass(listClass)
|
||||
? ObjectType.create(
|
||||
ClassType.cloneForSpecialization(
|
||||
listClass,
|
||||
[inferredEntryType],
|
||||
/* isTypeArgumentExplicit */ true,
|
||||
/* skipAbstractClassTest */ false,
|
||||
/* TupleTypeArguments */ undefined,
|
||||
isEmptyContainer
|
||||
)
|
||||
)
|
||||
: UnknownType.create();
|
||||
|
||||
return { type, node };
|
||||
}
|
||||
|
||||
@ -10258,7 +10309,8 @@ export function createTypeEvaluator(
|
||||
rule: string,
|
||||
target: NameNode,
|
||||
type: Type,
|
||||
errorNode: ExpressionNode
|
||||
errorNode: ExpressionNode,
|
||||
ignoreEmptyContainers: boolean
|
||||
) {
|
||||
// Don't bother if the feature is disabled.
|
||||
if (diagLevel === 'none') {
|
||||
@ -10275,19 +10327,24 @@ export function createTypeEvaluator(
|
||||
if (isUnknown(simplifiedType)) {
|
||||
addDiagnostic(diagLevel, rule, Localizer.Diagnostic.typeUnknown().format({ name: nameValue }), errorNode);
|
||||
} else if (isPartlyUnknown(simplifiedType)) {
|
||||
const diagAddendum = new DiagnosticAddendum();
|
||||
diagAddendum.addMessage(
|
||||
Localizer.DiagnosticAddendum.typeOfSymbol().format({
|
||||
name: nameValue,
|
||||
type: printType(simplifiedType, /* expandTypeAlias */ true),
|
||||
})
|
||||
);
|
||||
addDiagnostic(
|
||||
diagLevel,
|
||||
rule,
|
||||
Localizer.Diagnostic.typePartiallyUnknown().format({ name: nameValue }) + diagAddendum.getString(),
|
||||
errorNode
|
||||
);
|
||||
// If ignoreEmptyContainers is true, don't report the problem for
|
||||
// empty containers(lists or dictionaries). We'll report the problem
|
||||
// only if the assigned value is used later.
|
||||
if (!ignoreEmptyContainers || !isObject(type) || !type.classType.isEmptyContainer) {
|
||||
const diagAddendum = new DiagnosticAddendum();
|
||||
diagAddendum.addMessage(
|
||||
Localizer.DiagnosticAddendum.typeOfSymbol().format({
|
||||
name: nameValue,
|
||||
type: printType(simplifiedType, /* expandTypeAlias */ true),
|
||||
})
|
||||
);
|
||||
addDiagnostic(
|
||||
diagLevel,
|
||||
rule,
|
||||
Localizer.Diagnostic.typePartiallyUnknown().format({ name: nameValue }) + diagAddendum.getString(),
|
||||
errorNode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11366,7 +11423,8 @@ export function createTypeEvaluator(
|
||||
rightHandType,
|
||||
isIncomplete,
|
||||
node.rightExpression,
|
||||
expectedTypeDiagAddendum
|
||||
/* ignoreEmptyContainers */ true,
|
||||
expectedTypeDiagAddendum,
|
||||
);
|
||||
|
||||
writeTypeCache(node, rightHandType, isIncomplete);
|
||||
@ -13283,7 +13341,12 @@ export function createTypeEvaluator(
|
||||
});
|
||||
|
||||
if (node.name) {
|
||||
assignTypeToExpression(node.name, targetType, /* isIncomplete */ false, node.name);
|
||||
assignTypeToExpression(
|
||||
node.name,
|
||||
targetType,
|
||||
/* isIncomplete */ false,
|
||||
node.name
|
||||
);
|
||||
}
|
||||
|
||||
writeTypeCache(node, targetType, /* isIncomplete */ false);
|
||||
@ -13406,7 +13469,12 @@ export function createTypeEvaluator(
|
||||
});
|
||||
|
||||
if (node.target) {
|
||||
assignTypeToExpression(node.target, scopedType, !!exprTypeResult.isIncomplete, node.target);
|
||||
assignTypeToExpression(
|
||||
node.target,
|
||||
scopedType,
|
||||
!!exprTypeResult.isIncomplete,
|
||||
node.target
|
||||
);
|
||||
}
|
||||
|
||||
writeTypeCache(node, scopedType, !!exprTypeResult.isIncomplete);
|
||||
|
@ -354,6 +354,12 @@ export interface ClassType extends TypeBase {
|
||||
// some or all of the type parameters.
|
||||
typeArguments?: Type[];
|
||||
|
||||
// If a generic container class (like a list or dict) is known
|
||||
// to contain no elements, its type arguments may be "Unknown".
|
||||
// This value allows us to elide the Unknown when it's safe to
|
||||
// do so.
|
||||
isEmptyContainer?: boolean;
|
||||
|
||||
// For tuples, the class definition calls for a single type parameter but
|
||||
// the spec allows the programmer to provide variadic type arguments.
|
||||
// To make these compatible, we need to derive a single typeArgument value
|
||||
@ -425,7 +431,8 @@ export namespace ClassType {
|
||||
typeArguments: Type[] | undefined,
|
||||
isTypeArgumentExplicit: boolean,
|
||||
skipAbstractClassTest = false,
|
||||
tupleTypeArguments?: Type[]
|
||||
tupleTypeArguments?: Type[],
|
||||
isEmptyContainer?: boolean
|
||||
): ClassType {
|
||||
const newClassType = { ...classType };
|
||||
|
||||
@ -440,6 +447,10 @@ export namespace ClassType {
|
||||
? tupleTypeArguments.map((t) => (isNever(t) ? UnknownType.create() : t))
|
||||
: undefined;
|
||||
|
||||
if (isEmptyContainer !== undefined) {
|
||||
newClassType.isEmptyContainer = isEmptyContainer;
|
||||
}
|
||||
|
||||
return newClassType;
|
||||
}
|
||||
|
||||
@ -2059,7 +2070,7 @@ export function combineConstrainedTypes(subtypes: ConstrainedSubtype[], maxSubty
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all of the literal types to the end.
|
||||
// Sort all of the literal and empty types to the end.
|
||||
expandedTypes = expandedTypes.sort((constrainedType1, constrainedType2) => {
|
||||
const type1 = constrainedType1.type;
|
||||
const type2 = constrainedType2.type;
|
||||
@ -2074,6 +2085,13 @@ export function combineConstrainedTypes(subtypes: ConstrainedSubtype[], maxSubty
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (isObject(type1) && type1.classType.isEmptyContainer) {
|
||||
return 1;
|
||||
} else if (isObject(type2) && type2.classType.isEmptyContainer) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
@ -2199,6 +2217,14 @@ function _addTypeIfUnique(unionType: UnionType, typeToAdd: UnionableType, constr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the typeToAdd is an empty container and there's already
|
||||
// non-empty container of the same type, don't add the empty container.
|
||||
if (isObject(typeToAdd) && typeToAdd.classType.isEmptyContainer) {
|
||||
if (isObject(type) && ClassType.isSameGenericClass(type.classType, typeToAdd.classType)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnionType.addType(unionType, typeToAdd, constraintsToAdd);
|
||||
|
@ -0,0 +1,87 @@
|
||||
# This sample tests type inference for empty lists and dictionaries.
|
||||
|
||||
# pyright: reportUnknownVariableType=true, reportUnknownArgumentType=true
|
||||
|
||||
from typing import List, Literal
|
||||
|
||||
|
||||
def func1(a: bool):
|
||||
val1 = []
|
||||
|
||||
if a:
|
||||
val1 = [2, 3]
|
||||
|
||||
t_val1: Literal["list[int]"] = reveal_type(val1)
|
||||
|
||||
if a:
|
||||
val2 = []
|
||||
else:
|
||||
val2 = []
|
||||
|
||||
t_val2: Literal["list[Unknown]"] = reveal_type(val2)
|
||||
|
||||
# This should generate an error because val2 is partially unknown.
|
||||
val2 += [3]
|
||||
|
||||
val3 = val2
|
||||
|
||||
# This should generate an error because val3 is partially unknown.
|
||||
print(val3)
|
||||
t_val3_1: Literal["list[Unknown]"] = reveal_type(val3)
|
||||
|
||||
if a:
|
||||
val3 = [3.4]
|
||||
|
||||
print(val3)
|
||||
t_val3_2: Literal["list[float]"] = reveal_type(val3)
|
||||
|
||||
|
||||
def func2(a: bool):
|
||||
val1 = {}
|
||||
|
||||
if a:
|
||||
val1 = {"a": 2}
|
||||
|
||||
t_val1: Literal["dict[str, int]"] = reveal_type(val1)
|
||||
|
||||
if a:
|
||||
val2 = {}
|
||||
else:
|
||||
val2 = {}
|
||||
|
||||
t_val2: Literal["dict[Unknown, Unknown]"] = reveal_type(val2)
|
||||
|
||||
# This should generate an error because val2 is partially unknown.
|
||||
val2.pop()
|
||||
|
||||
val3 = val2
|
||||
|
||||
# This should generate an error because val3 is partially unknown.
|
||||
print(val3)
|
||||
t_val3_1: Literal["dict[Unknown, Unknown]"] = reveal_type(val3)
|
||||
|
||||
if a:
|
||||
val3 = {"b": 3.4}
|
||||
|
||||
print(val3)
|
||||
t_val3_2: Literal["dict[str, float]"] = reveal_type(val3)
|
||||
|
||||
|
||||
class A:
|
||||
def method1(self):
|
||||
self.val1 = []
|
||||
self.val2 = {}
|
||||
self.val3 = []
|
||||
|
||||
def method2(self):
|
||||
self.val1 = [3.4]
|
||||
self.val2 = {"a": 1}
|
||||
|
||||
def method3(self):
|
||||
t_val1: Literal["list[float]"] = reveal_type(self.val1)
|
||||
t_val2: Literal["dict[str, int]"] = reveal_type(self.val2)
|
||||
t_val3: Literal["list[Unknown]"] = reveal_type(self.val3)
|
||||
|
||||
def method4(self) -> List[int]:
|
||||
# This should generate an error because of a type mismatch.
|
||||
return self.val1
|
@ -1428,3 +1428,8 @@ test('Comparison1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['comparison1.py']);
|
||||
TestUtils.validateResults(analysisResults, 3);
|
||||
});
|
||||
|
||||
test('EmptyContainers1', () => {
|
||||
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['emptyContainers1.py']);
|
||||
TestUtils.validateResults(analysisResults, 5);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user