From 9c179a2d2ff0bd75000f874b746cb37f49156836 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:15:59 +0000 Subject: [PATCH 1/3] Fix pattern matching regression for variadic tuple types When a tuple with an unbounded entry (e.g., tuple[int, *tuple[str, ...], int]) is matched against a pattern shorter than the tuple, the unbounded entry is removed to make lengths match. However, this loses the information that the tuple was originally variadic and could have different lengths. The fix tracks when an unbounded entry is removed (removedIndeterminate flag) and marks such matches as isPotentialNoMatch=true. This prevents the entire variadic tuple from being eliminated in negative narrowing, preserving reachability of subsequent match cases. --- .../src/analyzer/patternMatching.ts | 14 ++++++++ .../tests/samples/matchSequenceVariadic.py | 34 +++++++++++++++++++ .../src/tests/typeEvaluator6.test.ts | 9 +++++ 3 files changed, 57 insertions(+) create mode 100644 packages/pyright-internal/src/tests/samples/matchSequenceVariadic.py diff --git a/packages/pyright-internal/src/analyzer/patternMatching.ts b/packages/pyright-internal/src/analyzer/patternMatching.ts index 4e4f05e97e6f..0319ff1bc2ac 100644 --- a/packages/pyright-internal/src/analyzer/patternMatching.ts +++ b/packages/pyright-internal/src/analyzer/patternMatching.ts @@ -1423,6 +1423,12 @@ function getSequencePatternInfo( // If the tuple contains an indeterminate entry, expand or remove that // entry to match the length of the pattern if possible. let expandedIndeterminate = false; + // Track when we remove an unbounded entry. When removedIndeterminate is true, + // tupleIndeterminateIndex becomes -1, making isUnboundedTuple false in the + // sequenceInfo entry. This is safe because isPotentialNoMatch=true (set below) + // prevents incorrect elimination at the isDefiniteMatch check before the + // isIndeterminateLength || isUnboundedTuple guard matters. + let removedIndeterminate = false; if (tupleIndeterminateIndex >= 0) { tupleDeterminateEntryCount--; @@ -1435,6 +1441,7 @@ function getSequencePatternInfo( if (typeArgs.length > patternEntryCount && patternStarEntryIndex === undefined) { typeArgs.splice(tupleIndeterminateIndex, 1); + removedIndeterminate = true; tupleIndeterminateIndex = -1; } } @@ -1474,6 +1481,13 @@ function getSequencePatternInfo( let isDefiniteNoMatch = false; let isPotentialNoMatch = tupleIndeterminateIndex >= 0; + // 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. diff --git a/packages/pyright-internal/src/tests/samples/matchSequenceVariadic.py b/packages/pyright-internal/src/tests/samples/matchSequenceVariadic.py new file mode 100644 index 000000000000..aa8788f184a0 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/matchSequenceVariadic.py @@ -0,0 +1,34 @@ +# This sample tests pattern matching with variadic tuple types. +# From the regression report for Pyright 1.1.408. + +from typing import TypeAlias, assert_type + +Func6Input: TypeAlias = tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int] + + +def func6(val: Func6Input): + match val: + case (x,): + # Type may be narrowed to tuple[int]. + # E: Argument of type "tuple[int]" cannot be assigned to parameter of type "tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]" + assert_type(val, Func6Input) + assert_type(val, tuple[int]) + + case (x, y): + # Type may be narrowed to tuple[str, str] | tuple[int, int]. + # E: Argument of type "tuple[str, str] | tuple[int, int]" cannot be assigned to parameter of type "tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]" + assert_type(val, Func6Input) + assert_type(val, tuple[str, str] | tuple[int, int]) + + case (x, y, z): + # Type may be narrowed to tuple[int, str, int]. + # This case should be reachable! + # E: Argument of type "tuple[int, str, int]" cannot be assigned to parameter of type "tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]" + assert_type(val, Func6Input) + assert_type(val, tuple[int, str, int]) + + case (w, x, y, z): + # Type may be narrowed to tuple[int, str, str, int]. + # E: Argument of type "tuple[int, str, str, int]" cannot be assigned to parameter of type "tuple[int] | tuple[str, str] | tuple[int, *tuple[str, ...], int]" + assert_type(val, Func6Input) + assert_type(val, tuple[int, str, str, int]) diff --git a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts index 577550e30bd4..f633c1a284d0 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator6.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator6.test.ts @@ -471,6 +471,15 @@ test('MatchSequence2', () => { TestUtils.validateResults(analysisResults, 0); }); +test('MatchSequenceVariadic', () => { + const configOptions = new ConfigOptions(Uri.empty()); + + configOptions.defaultPythonVersion = pythonVersion3_12; + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchSequenceVariadic.py'], configOptions); + // After fix: should be 4 errors and 0 unreachable code + TestUtils.validateResults(analysisResults, 4, 0, undefined, undefined, 0); +}); + test('MatchClass1', () => { const configOptions = new ConfigOptions(Uri.empty()); From 37c07699b80afb029aed44d69d51f4137373eec3 Mon Sep 17 00:00:00 2001 From: Graham Wheeler <> Date: Mon, 6 Apr 2026 17:34:57 -0700 Subject: [PATCH 2/3] Ignore agent files --- .gitignore | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6d9fd0bf3e8f..4fc9b28f88a5 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,10 @@ junit.xml Thumbs.db # Potential venv -.venv \ No newline at end of file +.venv + +# Agent files +serena/ +.beads/ +AGENTS.md + From 0375c449c18efc39677c4ae4432f88e3ff8a2498 Mon Sep 17 00:00:00 2001 From: Graham Wheeler <> Date: Thu, 9 Apr 2026 17:18:21 -0700 Subject: [PATCH 3/3] Address comments --- .gitignore | 8 +------- .../pyright-internal/src/analyzer/patternMatching.ts | 10 ++++------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 4fc9b28f88a5..6d9fd0bf3e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -125,10 +125,4 @@ junit.xml Thumbs.db # Potential venv -.venv - -# Agent files -serena/ -.beads/ -AGENTS.md - +.venv \ No newline at end of file diff --git a/packages/pyright-internal/src/analyzer/patternMatching.ts b/packages/pyright-internal/src/analyzer/patternMatching.ts index 0319ff1bc2ac..eda553456fd5 100644 --- a/packages/pyright-internal/src/analyzer/patternMatching.ts +++ b/packages/pyright-internal/src/analyzer/patternMatching.ts @@ -1423,11 +1423,9 @@ function getSequencePatternInfo( // If the tuple contains an indeterminate entry, expand or remove that // entry to match the length of the pattern if possible. let expandedIndeterminate = false; - // Track when we remove an unbounded entry. When removedIndeterminate is true, - // tupleIndeterminateIndex becomes -1, making isUnboundedTuple false in the - // sequenceInfo entry. This is safe because isPotentialNoMatch=true (set below) - // prevents incorrect elimination at the isDefiniteMatch check before the - // isIndeterminateLength || isUnboundedTuple guard matters. + // Track that the original tuple was variadic; this information is lost when + // the unbounded entry is spliced out, and is needed to prevent incorrect + // definite-match classification downstream. let removedIndeterminate = false; if (tupleIndeterminateIndex >= 0) { tupleDeterminateEntryCount--; @@ -1521,7 +1519,7 @@ function getSequencePatternInfo( entryTypes: isDefiniteNoMatch ? [] : typeArgs.map((t) => t.type), isIndeterminateLength: false, isTuple: true, - isUnboundedTuple: tupleIndeterminateIndex >= 0, + isUnboundedTuple: removedIndeterminate || tupleIndeterminateIndex >= 0, isDefiniteNoMatch, isPotentialNoMatch, });