diff --git a/pyrefly/lib/binding/pattern.rs b/pyrefly/lib/binding/pattern.rs index eb569835f9..c529729bc8 100644 --- a/pyrefly/lib/binding/pattern.rs +++ b/pyrefly/lib/binding/pattern.rs @@ -213,11 +213,17 @@ impl<'a> BindingsBuilder<'a> { Some(idx) => idx, Option::None => { // More patterns than tuple elements, skip narrowing - narrow_ops.and_all(self.bind_pattern( - MatchSubject::None, - x, - key_for_subpattern, - )); + for (name, (op, range)) in self + .bind_pattern( + MatchSubject::None, + x, + key_for_subpattern, + ) + .0 + { + let subject = NarrowingSubject::Name(name); + narrow_ops.and_for_subject(&subject, op, range); + } continue; } } @@ -238,11 +244,13 @@ impl<'a> BindingsBuilder<'a> { } _ => MatchSubject::None, }; - narrow_ops.and_all(self.bind_pattern( - subject_for_subpattern, - x, - key_for_subpattern, - )); + for (name, (op, range)) in self + .bind_pattern(subject_for_subpattern, x, key_for_subpattern) + .0 + { + let subject = NarrowingSubject::Name(name); + narrow_ops.and_for_subject(&subject, op, range); + } } } } @@ -599,11 +607,13 @@ impl<'a> BindingsBuilder<'a> { // shadow outer variables. When there is no narrowing subject // (e.g. `match make_color():`), drop all narrows so that alias // names don't resolve against unrelated outer variables. - new_narrow_ops.0.retain(|name, _| { - match_subject - .as_single() - .as_ref() - .is_some_and(|s| name == s.name()) + new_narrow_ops.0.retain(|name, _| match &match_subject { + MatchSubject::Single(subject) => name == subject.name(), + MatchSubject::Tuple(subjects) => subjects + .iter() + .flatten() + .any(|subject| name == subject.name()), + MatchSubject::None => false, }); negated_prev_ops.and_all(new_narrow_ops.negate()); self.stmts(body, parent); diff --git a/pyrefly/lib/lsp/non_wasm/server.rs b/pyrefly/lib/lsp/non_wasm/server.rs index b2cae30a3f..976c2c77b5 100644 --- a/pyrefly/lib/lsp/non_wasm/server.rs +++ b/pyrefly/lib/lsp/non_wasm/server.rs @@ -681,7 +681,11 @@ fn format_diagnostic_message_for_markdown(message: &str) -> String { #[cfg(test)] mod tests { + use lsp_types::CodeActionKind; + + use super::SOURCE_FIX_ALL_PYREFLY; use super::format_diagnostic_message_for_markdown; + use super::is_fix_all_code_action_kind_requested; #[test] fn test_format_diagnostic_message_for_markdown() { @@ -722,6 +726,26 @@ mod tests { fn test_format_only_special_characters() { assert_eq!(format_diagnostic_message_for_markdown("***"), "\\*\\*\\*"); } + + #[test] + fn test_fix_all_kind_filter_matches_supported_kinds() { + assert!(is_fix_all_code_action_kind_requested( + &CodeActionKind::SOURCE_FIX_ALL + )); + assert!(is_fix_all_code_action_kind_requested(&CodeActionKind::new( + SOURCE_FIX_ALL_PYREFLY, + ))); + } + + #[test] + fn test_fix_all_kind_filter_rejects_pyrefly_suffix_kinds() { + assert!(!is_fix_all_code_action_kind_requested( + &CodeActionKind::new("source.fixAll.pyrefly.foo",) + )); + assert!(!is_fix_all_code_action_kind_requested( + &CodeActionKind::new("source.fixAll.pyreflyyyyyy",) + )); + } } pub struct Server { @@ -1142,7 +1166,7 @@ pub fn capabilities( CodeActionKind::new("refactor.delete"), CodeActionKind::new("refactor.move"), CodeActionKind::REFACTOR_INLINE, - CodeActionKind::SOURCE_FIX_ALL, + CodeActionKind::new(SOURCE_FIX_ALL_PYREFLY), ]), ..Default::default() })), @@ -1252,6 +1276,15 @@ pub enum ProcessEvent { } const PYTHON_SECTION: &str = "python"; +const SOURCE_FIX_ALL_PYREFLY: &str = "source.fixAll.pyrefly"; + +fn is_fix_all_code_action_kind_requested(kind: &CodeActionKind) -> bool { + let requested = kind.as_str(); + requested == SOURCE_FIX_ALL_PYREFLY + || SOURCE_FIX_ALL_PYREFLY + .strip_prefix(requested) + .is_some_and(|suffix| suffix.starts_with('.')) +} struct TypeHierarchyTarget { def_index: ClassDefIndex, @@ -4170,11 +4203,8 @@ impl Server { let only_kinds = params.context.only.as_ref(); let allow_quickfix = only_kinds .is_none_or(|kinds| kinds.iter().any(|kind| kind == &CodeActionKind::QUICKFIX)); - let allow_fix_all = only_kinds.is_none_or(|kinds| { - kinds - .iter() - .any(|kind| kind == &CodeActionKind::SOURCE_FIX_ALL) - }); + let allow_fix_all = + only_kinds.is_none_or(|kinds| kinds.iter().any(is_fix_all_code_action_kind_requested)); let allow_refactor = only_kinds.is_none_or(|kinds| { kinds .iter() @@ -4276,7 +4306,7 @@ impl Server { if !changes.is_empty() { actions.push(CodeActionOrCommand::CodeAction(CodeAction { title: "Remove all redundant casts".to_owned(), - kind: Some(CodeActionKind::SOURCE_FIX_ALL), + kind: Some(CodeActionKind::new(SOURCE_FIX_ALL_PYREFLY)), edit: Some(WorkspaceEdit { changes: Some(changes), ..Default::default() diff --git a/pyrefly/lib/test/lsp/lsp_interaction/basic.rs b/pyrefly/lib/test/lsp/lsp_interaction/basic.rs index f9d4363b36..779f2fd4cf 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/basic.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/basic.rs @@ -36,7 +36,7 @@ fn test_initialize_basic() { "definitionProvider": true, "typeDefinitionProvider": true, "codeActionProvider": { - "codeActionKinds": ["quickfix", "refactor.extract", "refactor.rewrite", "refactor.delete", "refactor.move", "refactor.inline", "source.fixAll"] + "codeActionKinds": ["quickfix", "refactor.extract", "refactor.rewrite", "refactor.delete", "refactor.move", "refactor.inline", "source.fixAll.pyrefly"] }, "codeLensProvider": { "resolveProvider": false, diff --git a/pyrefly/lib/test/pattern_match.rs b/pyrefly/lib/test/pattern_match.rs index ab914733d3..2648e926ce 100644 --- a/pyrefly/lib/test/pattern_match.rs +++ b/pyrefly/lib/test/pattern_match.rs @@ -726,6 +726,27 @@ def test_sequence_after_none(seq_or_none: list[int] | None): "#, ); +testcase!( + test_match_tuple_subject_narrowing_after_none, + r#" +from typing import assert_type + +def test(a: list[int] | None, b: list[int] | None) -> None: + match a, b: + case None, None: + pass + case _, None: + assert_type(a, list[int]) + assert_type(b, None) + case None, _: + assert_type(a, None) + assert_type(b, list[int]) + case _: + assert_type(a, list[int]) + assert_type(b, list[int]) +"#, +); + testcase!( test_match_mapping_before_none, r#"