Skip to content

Commit 6c52d92

Browse files
.
1 parent 2f8b8e3 commit 6c52d92

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

pyrefly/lib/state/lsp/quick_fixes/invert_boolean.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ fn collect_load_edits(
262262
range: unary.range(),
263263
replacement: name.id.to_string(),
264264
});
265+
// Avoid also visiting the inner `abc` node and producing overlapping edits
266+
// (`not abc` -> `abc` and `abc` -> `not abc`) for the same source span.
265267
return;
266268
}
267269
if let Expr::Name(name) = expr

pyrefly/lib/test/lsp/code_actions.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::state::lsp::LocalRefactorCodeAction;
1717
use crate::state::require::Require;
1818
use crate::state::state::State;
1919
use crate::test::util::get_batched_lsp_operations_report_allow_error;
20+
use crate::test::util::mk_multi_file_state;
2021
use crate::test::util::mk_multi_file_state_assert_no_errors;
2122

2223
fn apply_patch(info: &ModuleInfo, range: TextRange, patch: String) -> (String, String) {
@@ -215,6 +216,36 @@ fn assert_no_invert_boolean_action(code: &str, selection: TextRange) {
215216
);
216217
}
217218

219+
fn compute_invert_boolean_actions_allow_errors(
220+
code: &str,
221+
selection: TextRange,
222+
) -> (
223+
ModuleInfo,
224+
Vec<Vec<(Module, TextRange, String)>>,
225+
Vec<String>,
226+
) {
227+
let (handles, state) = mk_multi_file_state(&[("main", code)], Require::Everything, false);
228+
let handle = handles.get("main").unwrap();
229+
let transaction = state.transaction();
230+
let module_info = transaction.get_module_info(handle).unwrap();
231+
let actions = transaction
232+
.invert_boolean_code_actions(handle, selection)
233+
.unwrap_or_default();
234+
let edit_sets: Vec<Vec<(Module, TextRange, String)>> =
235+
actions.iter().map(|action| action.edits.clone()).collect();
236+
let titles = actions.iter().map(|action| action.title.clone()).collect();
237+
(module_info, edit_sets, titles)
238+
}
239+
240+
fn assert_no_invert_boolean_action_allow_errors(code: &str, selection: TextRange) {
241+
let (_, actions, _) = compute_invert_boolean_actions_allow_errors(code, selection);
242+
assert!(
243+
actions.is_empty(),
244+
"expected no invert-boolean actions, found {}",
245+
actions.len()
246+
);
247+
}
248+
218249
fn assert_no_extract_variable_action(code: &str) {
219250
let (_, actions, _) = compute_extract_variable_actions(code);
220251
assert!(
@@ -915,6 +946,94 @@ def foo():
915946
assert_no_invert_boolean_action(code, selection);
916947
}
917948

949+
#[test]
950+
fn invert_boolean_annotated_assignment() {
951+
let code = r#"
952+
def foo():
953+
abc: bool = True
954+
return abc
955+
"#;
956+
let selection = find_nth_range(code, "abc", 2);
957+
let updated =
958+
apply_first_invert_boolean_action(code, selection).expect("expected invert-boolean action");
959+
let expected = r#"
960+
def foo():
961+
abc: bool = False
962+
return (not abc)
963+
"#;
964+
assert_eq!(expected.trim(), updated.trim());
965+
}
966+
967+
#[test]
968+
fn invert_boolean_rejects_deleted_variable() {
969+
let code = r#"
970+
def foo():
971+
abc = True
972+
del abc
973+
return abc
974+
"#;
975+
let selection = find_nth_range(code, "abc", 3);
976+
assert_no_invert_boolean_action_allow_errors(code, selection);
977+
}
978+
979+
#[test]
980+
fn invert_boolean_multiple_assignments() {
981+
let code = r#"
982+
def foo():
983+
abc = True
984+
abc = False
985+
return abc
986+
"#;
987+
let selection = find_nth_range(code, "abc", 3);
988+
let updated =
989+
apply_first_invert_boolean_action(code, selection).expect("expected invert-boolean action");
990+
let expected = r#"
991+
def foo():
992+
abc = True
993+
abc = True
994+
return (not abc)
995+
"#;
996+
assert_eq!(expected.trim(), updated.trim());
997+
}
998+
999+
#[test]
1000+
fn invert_boolean_nested_expression_keeps_outer_not() {
1001+
let code = r#"
1002+
def foo():
1003+
abc = True
1004+
other = True
1005+
return not (abc and other)
1006+
"#;
1007+
let selection = find_nth_range(code, "abc", 2);
1008+
let updated =
1009+
apply_first_invert_boolean_action(code, selection).expect("expected invert-boolean action");
1010+
let expected = r#"
1011+
def foo():
1012+
abc = False
1013+
other = True
1014+
return not ((not abc) and other)
1015+
"#;
1016+
assert_eq!(expected.trim(), updated.trim());
1017+
}
1018+
1019+
#[test]
1020+
fn invert_boolean_inverts_unary_not_assignment_value() {
1021+
let code = r#"
1022+
def foo(other_var):
1023+
abc = not other_var
1024+
return abc
1025+
"#;
1026+
let selection = find_nth_range(code, "abc", 2);
1027+
let updated =
1028+
apply_first_invert_boolean_action(code, selection).expect("expected invert-boolean action");
1029+
let expected = r#"
1030+
def foo(other_var):
1031+
abc = other_var
1032+
return (not abc)
1033+
"#;
1034+
assert_eq!(expected.trim(), updated.trim());
1035+
}
1036+
9181037
#[test]
9191038
fn pull_member_up_basic() {
9201039
let code = r#"

0 commit comments

Comments
 (0)