Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions packages/pyright-internal/src/analyzer/typeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,6 @@ export interface AddConditionOptions {
skipBoundTypeVars?: boolean;
}

// There are cases where tuple types can be infinitely nested. The
// recursion count limit will eventually be hit, but this will create
// deep types that will effectively hang the analyzer. To prevent this,
// we'll limit the depth of the tuple type arguments. This value is
// large enough that we should never hit it in legitimate circumstances.
const maxTupleTypeArgRecursionDepth = 10;

// Tracks whether a function signature has been seen before within
// an expression. For example, in the expression "foo(foo, foo)", the
// signature for "foo" will be seen three times at three different
Expand Down Expand Up @@ -3723,10 +3716,6 @@ export class TypeVarTransformer {

// Handle tuples specially.
if (ClassType.isTupleClass(classType)) {
if (getContainerDepth(classType) > maxTupleTypeArgRecursionDepth) {
return classType;
}

if (classType.priv.tupleTypeArgs) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this early-return fixes the escaping TypeVar (1→0 errors), but it also deletes a documented performance safeguard whose comment explicitly claimed the recursion-count limit is insufficient ("will effectively hang the analyzer"). Consider a narrower, safe-bailout variant: keep a depth bound but make the bailout safe (e.g. return Unknown or still solve top-level TypeVars) so nothing escapes at any depth while preserving the perf guard. Since this is vendored, upstream-owned Pyright core (keep diffs minimal, coordinate upstream), prefer the safe-bailout variant or coordinate the bound removal upstream against the original motivating repro, and confirm that repro still terminates without the guard.

newTupleTypeArgs = [];

Expand Down
73 changes: 73 additions & 0 deletions packages/pyright-internal/src/tests/samples/typeVarTuple31.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# This sample tests the case where a TypeVarTuple is used in a method
# called on an instance whose type arguments contain deeply nested
# recursive tuple type aliases. The deep nesting previously caused the
# solved TypeVar to "escape" because type var transformation bailed out
# early on deeply nested tuples.

from typing import final, override
from collections.abc import Callable


@final
class Ok[T]:
__match_args__ = ("_value",)

def __init__(self, value: T):
self._value: T = value


@final
class Err[E]:
def __init__(self, value: E):
self._value: E = value


type Result[T, E] = Ok[T] | Err[E]

type ParserResult[O, E] = Result[tuple[int, O], E]
type ParserFunc[O, E] = Callable[[str, int], ParserResult[O, E]]


class Parser[O, E]:
def __init__(self, func: ParserFunc[O, E]):
self._func: ParserFunc[O, E] = func

def then[OO, OE](self, _other: "Parser[OO, OE]") -> "Parser[tuple[O, OO], E | OE]":
raise NotImplementedError

def unpack_then[*TS, OO, OE](
self: "Parser[tuple[*TS], E]", _other: "Parser[OO, OE]"
) -> "Parser[tuple[*TS, OO], E | OE]":
raise NotImplementedError


def produce[T](_: T | None = None) -> T:
raise NotImplementedError


class ForwardRefParser[O, E](Parser[O, E]):
@override
def __init__(self, func: Callable[[], Parser[O, E]]):
self._meta_func: Callable[[], Parser[O, E]] = func
super().__init__(produce())


type Whitespace = tuple[tuple[tuple[tuple[Whitespace]]]]
type Implementations = tuple[tuple[tuple[Whitespace], Implementations]]
type BlockItem = tuple[tuple[Implementations]] | tuple[BlockItem]

ws: ForwardRefParser[Whitespace, None] = produce()
implementations: ForwardRefParser[Implementations, None] = produce()
block: Parser[tuple[BlockItem], None] = produce()

# This should not generate an error. Previously the "OO" TypeVar from
# "unpack_then" escaped into the inferred type of this assignment.
forloop: ForwardRefParser[
tuple[
Whitespace,
Whitespace,
tuple[BlockItem],
Whitespace,
],
None,
] = ForwardRefParser(lambda: ws.then(ws).unpack_then(block).unpack_then(ws))
8 changes: 8 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator6.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ test('TypeVarTuple30', () => {
TestUtils.validateResults(analysisResults, 0);
});

test('TypeVarTuple31', () => {
const configOptions = new ConfigOptions(Uri.empty());

configOptions.defaultPythonVersion = pythonVersion3_12;
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarTuple31.py'], configOptions);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateResults(analysisResults, 0) asserts only "no errors" — it would still pass if OO were replaced by some other wrong-but-non-erroring type. For a TypeVar-escape bug, add a reveal_type (or hover) assertion pinning forloop's concrete inferred type so the fix's correctness is self-evident and a future divergent-but-non-error regression is caught.

TestUtils.validateResults(analysisResults, 0);
});

test('Match1', () => {
const configOptions = new ConfigOptions(Uri.empty());

Expand Down
Loading