Skip to content

Commit c0b4ef2

Browse files
committed
Fixed bug that led to a false positive error when a TypeVar was bound to StringLiteral and conditionally narrowed. This addresses microsoft/pyright#4857.
1 parent 82d3419 commit c0b4ef2

File tree

4 files changed

+77
-2
lines changed

4 files changed

+77
-2
lines changed

packages/pyright-internal/src/analyzer/typeEvaluator.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ import {
284284
isTypeAliasPlaceholder,
285285
isTypeAliasRecursive,
286286
isTypeVarLimitedToCallable,
287+
isTypeVarSame,
287288
isUnboundedTupleClass,
288289
isUnionableType,
289290
isVarianceOfTypeArgumentCompatible,
@@ -1514,7 +1515,13 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
15141515
if (ClassType.isBuiltIn(subtype, 'LiteralString')) {
15151516
// Handle "LiteralString" specially.
15161517
if (strClassType && isInstantiableClass(strClassType)) {
1517-
return ClassType.cloneAsInstance(strClassType);
1518+
let strInstance = ClassType.cloneAsInstance(strClassType);
1519+
1520+
if (subtype.condition) {
1521+
strInstance = TypeBase.cloneForCondition(strInstance, getTypeCondition(subtype));
1522+
}
1523+
1524+
return strInstance;
15181525
}
15191526
}
15201527
}
@@ -22690,7 +22697,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
2269022697
// Before performing any other checks, see if the dest type is a
2269122698
// TypeVar that we are attempting to match.
2269222699
if (isTypeVar(destType)) {
22693-
if (isTypeSame(destType, srcType)) {
22700+
if (isTypeVarSame(destType, srcType)) {
2269422701
if (destType.scopeId && destTypeVarContext?.hasSolveForScope(destType.scopeId)) {
2269522702
return assignTypeToTypeVar(
2269622703
evaluatorInterface,

packages/pyright-internal/src/analyzer/typeUtils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,42 @@ export function isIncompleteUnknown(type: Type): boolean {
215215
return isUnknown(type) && type.isIncomplete;
216216
}
217217

218+
// Similar to isTypeSame except that type1 is a TypeVar and type2
219+
// can be either a TypeVar of the same type or a union that includes
220+
// conditional types associated with that bound TypeVar.
221+
export function isTypeVarSame(type1: TypeVarType, type2: Type) {
222+
if (isTypeSame(type1, type2)) {
223+
return true;
224+
}
225+
226+
// If this isn't a bound TypeVar, return false.
227+
if (type1.details.isParamSpec || type1.details.isVariadic || !type1.details.boundType) {
228+
return false;
229+
}
230+
231+
// If the second type isn't a union, return false.
232+
if (!isUnion(type2)) {
233+
return false;
234+
}
235+
236+
let isCompatible = true;
237+
doForEachSubtype(type2, (subtype) => {
238+
if (!isCompatible) {
239+
return;
240+
}
241+
242+
if (!isTypeSame(type1, subtype)) {
243+
const conditions = getTypeCondition(subtype);
244+
245+
if (!conditions || !conditions.some((condition) => condition.typeVarName === type1.nameWithScope)) {
246+
isCompatible = false;
247+
}
248+
}
249+
});
250+
251+
return isCompatible;
252+
}
253+
218254
export function makeInferenceContext(
219255
expectedType: undefined,
220256
typeVarContext?: TypeVarContext,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This sample tests the case where a LiteralString is used as the bound
2+
# of a TypeVar.
3+
4+
from typing import Generic, TypeVar, LiteralString
5+
6+
T = TypeVar("T")
7+
T_LS = TypeVar("T_LS", bound=LiteralString)
8+
9+
10+
class ClassA(Generic[T]):
11+
def __init__(self, val: T) -> None:
12+
...
13+
14+
15+
def func1(x: T) -> ClassA[T]:
16+
return ClassA(x)
17+
18+
19+
def func2(x: T_LS | None, default: T_LS) -> ClassA[T_LS]:
20+
if x is None:
21+
x = default
22+
23+
reveal_type(x, expected_text="T_LS@func2 | LiteralString*")
24+
out = func1(x)
25+
reveal_type(out, expected_text="ClassA[T_LS@func2 | str*]")
26+
return out

packages/pyright-internal/src/tests/typeEvaluator1.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,12 @@ test('LiteralString2', () => {
14581458
TestUtils.validateResults(analysisResults, 0);
14591459
});
14601460

1461+
test('LiteralString3', () => {
1462+
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['literalString3.py']);
1463+
1464+
TestUtils.validateResults(analysisResults, 0);
1465+
});
1466+
14611467
test('ParamInference1', () => {
14621468
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramInference1.py']);
14631469

0 commit comments

Comments
 (0)