Skip to content

Preserve tuple match narrows across subsequent cases#3224

Open
Arths17 wants to merge 6 commits intofacebook:mainfrom
Arths17:fix-tuple-match-none-narrowing
Open

Preserve tuple match narrows across subsequent cases#3224
Arths17 wants to merge 6 commits intofacebook:mainfrom
Arths17:fix-tuple-match-none-narrowing

Conversation

@Arths17
Copy link
Copy Markdown
Contributor

@Arths17 Arths17 commented Apr 24, 2026

This keeps per-element narrows from tuple match cases alive when control falls through to later cases, so a case None, None no longer drops the surviving type information for the remaining branches.

Fixes #3213.

Arths17 added 4 commits April 7, 2026 01:15
Code action save hooks currently require enabling generic source.fixAll, which can trigger unrelated servers. This change advertises and emits source.fixAll.pyrefly so editors can target pyrefly-only fix-all behavior.\n\nThe code action filter now accepts both source.fixAll and source.fixAll.* requests so parent-kind clients remain compatible while pyrefly-specific requests are supported.
When a client requests source.fixAll.*, we should not run pyrefly fix-all for sibling providers. Restrict matching to either the generic source.fixAll ancestor or source.fixAll.pyrefly descendants.
Reviewer feedback pointed out that prefix matching for source.fixAll.pyrefly would accept unrelated kinds such as source.fixAll.pyrefly.foo. This makes fix-all behavior broader than intended.

Use exact matching for source.fixAll.pyrefly (while still accepting source.fixAll), and add regression tests that prove accepted and rejected kind values. This keeps code-action filtering aligned with LSP expectations and prevents false-positive fix-all triggers.
Keep tuple-element narrows alive when a match case falls through to later cases, so `None` cases on multi-subject matches do not lose the surviving element types.
Copilot AI review requested due to automatic review settings April 24, 2026 04:37
@meta-cla meta-cla Bot added the cla signed label Apr 24, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to preserve type narrowing information across subsequent match cases when the match subject is a tuple (e.g. match a, b:), addressing false positives like those described in #3213. It also updates the LSP “fix all” code action kind to a Pyrefly-specific kind.

Changes:

  • Add a regression test covering tuple-subject narrowing across later match cases after a None, None case.
  • Update LSP server capabilities and emitted code actions to use source.fixAll.pyrefly, with new unit tests around kind filtering.
  • Adjust pattern binding logic to retain and propagate narrowing ops for tuple match subjects across cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
pyrefly/lib/test/pattern_match.rs Adds a regression test for tuple-subject narrowing after a None, None match case.
pyrefly/lib/binding/pattern.rs Changes how tuple-subject match narrows are accumulated/retained across cases.
pyrefly/lib/lsp/non_wasm/server.rs Switches fix-all code action kind to source.fixAll.pyrefly and adds filtering tests.
pyrefly/lib/test/lsp/lsp_interaction/basic.rs Updates LSP initialization capability expectations for the new fix-all kind.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +222 to +226
.0
{
let subject = NarrowingSubject::Name(name);
narrow_ops.and_for_subject(&subject, op, range);
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

In tuple/sequence pattern handling, this change switches from and_all to per-entry and_for_subject, which avoids Placeholder insertion. That means a multi-element pattern like case None, None will produce independent narrows for both names, and when stmt_match negates them for fallthrough it becomes a is not None AND b is not None (conjunction), which is not the logical negation of (a is None) AND (b is None). This can make later cases like case _, None incorrectly treat b as Never and can hide type errors. Consider representing fallthrough constraints at the tuple-pattern level (or otherwise avoid per-name negation for multi-name patterns) so the complement is modeled correctly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Comment on lines +249 to +253
.0
{
let subject = NarrowingSubject::Name(name);
narrow_ops.and_for_subject(&subject, op, range);
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Same issue as above: collecting subpattern narrows without Placeholder insertion makes subsequent fallthrough narrowing treat per-element negations as conjunctive constraints. For tuple subjects this is not a sound representation of case exclusion and can lead to contradictory narrows (e.g., b != None carried into a case _, None).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Comment on lines +611 to +615
MatchSubject::Single(subject) => name == subject.name(),
MatchSubject::Tuple(subjects) => subjects
.iter()
.flatten()
.any(|subject| name == subject.name()),
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Retaining per-element narrows for MatchSubject::Tuple means negated_prev_ops will accumulate per-name negations across cases, but those negations are applied conjunctively (bind_narrow_ops loops over names). For a prior case that narrowed multiple tuple elements (e.g. None, None), the correct fallthrough condition is a disjunction across elements, not a != None && b != None. This can make later cases unreachable or produce Never types. A different representation of tuple-case fallthrough (not per-name conjunction) is needed for sound narrowing across cases.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Comment thread pyrefly/lib/test/pattern_match.rs
Comment thread pyrefly/lib/lsp/non_wasm/server.rs Outdated
Arths17 and others added 2 commits April 23, 2026 23:44
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added size/m and removed size/m labels Apr 24, 2026
@github-actions
Copy link
Copy Markdown

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Copy link
Copy Markdown
Contributor

@kinto0 kinto0 left a comment

Choose a reason for hiding this comment

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

thanks for the PR! i'll assign it to you if you comment on the issue (github doesn't let us unless you comment). looks like some tests are failing and I can take a closer look after that is resolved

@kinto0 kinto0 self-assigned this Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pyrefly should narrow subsequent match cases after None case

3 participants