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:
Eric Traut 2021-03-24 13:50:59 -07:00
parent ca2e35d217
commit e5714b3365
5 changed files with 229 additions and 29 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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);
});