From 39728a1198f1ed6081a0fd159715737848a50aa2 Mon Sep 17 00:00:00 2001 From: Jesse Serrao Date: Sat, 11 Nov 2023 00:29:23 +0000 Subject: [PATCH] Add check for is comparison with mutable initialisers to rule F632 (#8607) ## Summary Adds an extra check to F632 to check for any `is` comparisons to a mutable initialisers. Implements #8589 . Example: ```Python named_var = {} if named_var is {}: # F632 (fix) pass ``` The if condition will always evaluate to False because it checks on identity and it's impossible to take the same identity as a hard coded list/set/dict initializer. ## Test Plan Multiple test cases were added to ensure the rule works + doesn't flag false positives + the fix works correctly. --- .../fixtures/pycodestyle/constant_literals.py | 84 +++ .../ruff_linter/src/rules/pycodestyle/mod.rs | 1 + ...pycodestyle__tests__constant_literals.snap | 4 +- ...s__preview__F632_constant_literals.py.snap | 481 ++++++++++++++++++ .../rules/invalid_literal_comparisons.rs | 14 +- crates/ruff_python_ast/src/helpers.rs | 13 + 6 files changed, 594 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__F632_constant_literals.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/constant_literals.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/constant_literals.py index 4c8dc7d10b856..3dfa6bbdbca01 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/constant_literals.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/constant_literals.py @@ -16,6 +16,48 @@ if None == False: # E711, E712 (fix) pass +named_var = [] +if [] is []: # F632 (fix) + pass +if named_var is []: # F632 (fix) + pass +if [] is named_var: # F632 (fix) + pass +if named_var is [1]: # F632 (fix) + pass +if [1] is named_var: # F632 (fix) + pass +if named_var is [i for i in [1]]: # F632 (fix) + pass + +named_var = {} +if {} is {}: # F632 (fix) + pass +if named_var is {}: # F632 (fix) + pass +if {} is named_var: # F632 (fix) + pass +if named_var is {1}: # F632 (fix) + pass +if {1} is named_var: # F632 (fix) + pass +if named_var is {i for i in [1]}: # F632 (fix) + pass + +named_var = {1: 1} +if {1: 1} is {1: 1}: # F632 (fix) + pass +if named_var is {1: 1}: # F632 (fix) + pass +if {1: 1} is named_var: # F632 (fix) + pass +if named_var is {1: 1}: # F632 (fix) + pass +if {1: 1} is named_var: # F632 (fix) + pass +if named_var is {i: 1 for i in [1]}: # F632 (fix) + pass + ### # Non-errors ### @@ -33,3 +75,45 @@ pass if None is False: pass + +named_var = [] +if [] == []: + pass +if named_var == []: + pass +if [] == named_var: + pass +if named_var == [1]: + pass +if [1] == named_var: + pass +if named_var == [i for i in [1]]: + pass + +named_var = {} +if {} == {}: + pass +if named_var == {}: + pass +if {} == named_var: + pass +if named_var == {1}: + pass +if {1} == named_var: + pass +if named_var == {i for i in [1]}: + pass + +named_var = {1: 1} +if {1: 1} == {1: 1}: + pass +if named_var == {1: 1}: + pass +if {1: 1} == named_var: + pass +if named_var == {1: 1}: + pass +if {1: 1} == named_var: + pass +if named_var == {i: 1 for i in [1]}: + pass diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index d91a46ff81fcb..583e696a36b0c 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -63,6 +63,7 @@ mod tests { Ok(()) } + #[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))] #[test_case(Rule::TypeComparison, Path::new("E721.py"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap index cf0ce6195f518..ddd0e98292906 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__constant_literals.snap @@ -166,7 +166,7 @@ constant_literals.py:16:4: E711 [*] Comparison to `None` should be `cond is None 16 |+if None is False: # E711, E712 (fix) 17 17 | pass 18 18 | -19 19 | ### +19 19 | named_var = [] constant_literals.py:16:12: E712 [*] Comparison to `False` should be `cond is False` or `if not cond:` | @@ -186,6 +186,6 @@ constant_literals.py:16:12: E712 [*] Comparison to `False` should be `cond is Fa 16 |+if None is False: # E711, E712 (fix) 17 17 | pass 18 18 | -19 19 | ### +19 19 | named_var = [] diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__F632_constant_literals.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__F632_constant_literals.py.snap new file mode 100644 index 0000000000000..fb6fc26d24c06 --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__F632_constant_literals.py.snap @@ -0,0 +1,481 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +--- +constant_literals.py:4:4: F632 [*] Use `==` to compare constant literals + | +2 | # Errors +3 | ### +4 | if "abc" is "def": # F632 (fix) + | ^^^^^^^^^^^^^^ F632 +5 | pass +6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) + | + = help: Replace `is` with `==` + +ℹ Safe fix +1 1 | ### +2 2 | # Errors +3 3 | ### +4 |-if "abc" is "def": # F632 (fix) + 4 |+if "abc" == "def": # F632 (fix) +5 5 | pass +6 6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) +7 7 | pass + +constant_literals.py:6:4: F632 [*] Use `==` to compare constant literals + | +4 | if "abc" is "def": # F632 (fix) +5 | pass +6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) + | ^^^^^^^^^^^^^ F632 +7 | pass +8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) + | + = help: Replace `is` with `==` + +ℹ Safe fix +3 3 | ### +4 4 | if "abc" is "def": # F632 (fix) +5 5 | pass +6 |-if "abc" is None: # F632 (fix, but leaves behind unfixable E711) + 6 |+if "abc" == None: # F632 (fix, but leaves behind unfixable E711) +7 7 | pass +8 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) +9 9 | pass + +constant_literals.py:8:4: F632 [*] Use `==` to compare constant literals + | + 6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) + 7 | pass + 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) + | ^^^^^^^^^^^^^ F632 + 9 | pass +10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) + | + = help: Replace `is` with `==` + +ℹ Safe fix +5 5 | pass +6 6 | if "abc" is None: # F632 (fix, but leaves behind unfixable E711) +7 7 | pass +8 |-if None is "abc": # F632 (fix, but leaves behind unfixable E711) + 8 |+if None == "abc": # F632 (fix, but leaves behind unfixable E711) +9 9 | pass +10 10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) +11 11 | pass + +constant_literals.py:10:4: F632 [*] Use `==` to compare constant literals + | + 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) + 9 | pass +10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) + | ^^^^^^^^^^^^^^ F632 +11 | pass +12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) + | + = help: Replace `is` with `==` + +ℹ Safe fix +7 7 | pass +8 8 | if None is "abc": # F632 (fix, but leaves behind unfixable E711) +9 9 | pass +10 |-if "abc" is False: # F632 (fix, but leaves behind unfixable E712) + 10 |+if "abc" == False: # F632 (fix, but leaves behind unfixable E712) +11 11 | pass +12 12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) +13 13 | pass + +constant_literals.py:12:4: F632 [*] Use `==` to compare constant literals + | +10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) +11 | pass +12 | if False is "abc": # F632 (fix, but leaves behind unfixable E712) + | ^^^^^^^^^^^^^^ F632 +13 | pass +14 | if False == None: # E711, E712 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +9 9 | pass +10 10 | if "abc" is False: # F632 (fix, but leaves behind unfixable E712) +11 11 | pass +12 |-if False is "abc": # F632 (fix, but leaves behind unfixable E712) + 12 |+if False == "abc": # F632 (fix, but leaves behind unfixable E712) +13 13 | pass +14 14 | if False == None: # E711, E712 (fix) +15 15 | pass + +constant_literals.py:20:4: F632 [*] Use `==` to compare constant literals + | +19 | named_var = [] +20 | if [] is []: # F632 (fix) + | ^^^^^^^^ F632 +21 | pass +22 | if named_var is []: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +17 17 | pass +18 18 | +19 19 | named_var = [] +20 |-if [] is []: # F632 (fix) + 20 |+if [] == []: # F632 (fix) +21 21 | pass +22 22 | if named_var is []: # F632 (fix) +23 23 | pass + +constant_literals.py:22:4: F632 [*] Use `==` to compare constant literals + | +20 | if [] is []: # F632 (fix) +21 | pass +22 | if named_var is []: # F632 (fix) + | ^^^^^^^^^^^^^^^ F632 +23 | pass +24 | if [] is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +19 19 | named_var = [] +20 20 | if [] is []: # F632 (fix) +21 21 | pass +22 |-if named_var is []: # F632 (fix) + 22 |+if named_var == []: # F632 (fix) +23 23 | pass +24 24 | if [] is named_var: # F632 (fix) +25 25 | pass + +constant_literals.py:24:4: F632 [*] Use `==` to compare constant literals + | +22 | if named_var is []: # F632 (fix) +23 | pass +24 | if [] is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^ F632 +25 | pass +26 | if named_var is [1]: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +21 21 | pass +22 22 | if named_var is []: # F632 (fix) +23 23 | pass +24 |-if [] is named_var: # F632 (fix) + 24 |+if [] == named_var: # F632 (fix) +25 25 | pass +26 26 | if named_var is [1]: # F632 (fix) +27 27 | pass + +constant_literals.py:26:4: F632 [*] Use `==` to compare constant literals + | +24 | if [] is named_var: # F632 (fix) +25 | pass +26 | if named_var is [1]: # F632 (fix) + | ^^^^^^^^^^^^^^^^ F632 +27 | pass +28 | if [1] is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +23 23 | pass +24 24 | if [] is named_var: # F632 (fix) +25 25 | pass +26 |-if named_var is [1]: # F632 (fix) + 26 |+if named_var == [1]: # F632 (fix) +27 27 | pass +28 28 | if [1] is named_var: # F632 (fix) +29 29 | pass + +constant_literals.py:28:4: F632 [*] Use `==` to compare constant literals + | +26 | if named_var is [1]: # F632 (fix) +27 | pass +28 | if [1] is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^^ F632 +29 | pass +30 | if named_var is [i for i in [1]]: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +25 25 | pass +26 26 | if named_var is [1]: # F632 (fix) +27 27 | pass +28 |-if [1] is named_var: # F632 (fix) + 28 |+if [1] == named_var: # F632 (fix) +29 29 | pass +30 30 | if named_var is [i for i in [1]]: # F632 (fix) +31 31 | pass + +constant_literals.py:30:4: F632 [*] Use `==` to compare constant literals + | +28 | if [1] is named_var: # F632 (fix) +29 | pass +30 | if named_var is [i for i in [1]]: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F632 +31 | pass + | + = help: Replace `is` with `==` + +ℹ Safe fix +27 27 | pass +28 28 | if [1] is named_var: # F632 (fix) +29 29 | pass +30 |-if named_var is [i for i in [1]]: # F632 (fix) + 30 |+if named_var == [i for i in [1]]: # F632 (fix) +31 31 | pass +32 32 | +33 33 | named_var = {} + +constant_literals.py:34:4: F632 [*] Use `==` to compare constant literals + | +33 | named_var = {} +34 | if {} is {}: # F632 (fix) + | ^^^^^^^^ F632 +35 | pass +36 | if named_var is {}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +31 31 | pass +32 32 | +33 33 | named_var = {} +34 |-if {} is {}: # F632 (fix) + 34 |+if {} == {}: # F632 (fix) +35 35 | pass +36 36 | if named_var is {}: # F632 (fix) +37 37 | pass + +constant_literals.py:36:4: F632 [*] Use `==` to compare constant literals + | +34 | if {} is {}: # F632 (fix) +35 | pass +36 | if named_var is {}: # F632 (fix) + | ^^^^^^^^^^^^^^^ F632 +37 | pass +38 | if {} is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +33 33 | named_var = {} +34 34 | if {} is {}: # F632 (fix) +35 35 | pass +36 |-if named_var is {}: # F632 (fix) + 36 |+if named_var == {}: # F632 (fix) +37 37 | pass +38 38 | if {} is named_var: # F632 (fix) +39 39 | pass + +constant_literals.py:38:4: F632 [*] Use `==` to compare constant literals + | +36 | if named_var is {}: # F632 (fix) +37 | pass +38 | if {} is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^ F632 +39 | pass +40 | if named_var is {1}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +35 35 | pass +36 36 | if named_var is {}: # F632 (fix) +37 37 | pass +38 |-if {} is named_var: # F632 (fix) + 38 |+if {} == named_var: # F632 (fix) +39 39 | pass +40 40 | if named_var is {1}: # F632 (fix) +41 41 | pass + +constant_literals.py:40:4: F632 [*] Use `==` to compare constant literals + | +38 | if {} is named_var: # F632 (fix) +39 | pass +40 | if named_var is {1}: # F632 (fix) + | ^^^^^^^^^^^^^^^^ F632 +41 | pass +42 | if {1} is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +37 37 | pass +38 38 | if {} is named_var: # F632 (fix) +39 39 | pass +40 |-if named_var is {1}: # F632 (fix) + 40 |+if named_var == {1}: # F632 (fix) +41 41 | pass +42 42 | if {1} is named_var: # F632 (fix) +43 43 | pass + +constant_literals.py:42:4: F632 [*] Use `==` to compare constant literals + | +40 | if named_var is {1}: # F632 (fix) +41 | pass +42 | if {1} is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^^ F632 +43 | pass +44 | if named_var is {i for i in [1]}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +39 39 | pass +40 40 | if named_var is {1}: # F632 (fix) +41 41 | pass +42 |-if {1} is named_var: # F632 (fix) + 42 |+if {1} == named_var: # F632 (fix) +43 43 | pass +44 44 | if named_var is {i for i in [1]}: # F632 (fix) +45 45 | pass + +constant_literals.py:44:4: F632 [*] Use `==` to compare constant literals + | +42 | if {1} is named_var: # F632 (fix) +43 | pass +44 | if named_var is {i for i in [1]}: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F632 +45 | pass + | + = help: Replace `is` with `==` + +ℹ Safe fix +41 41 | pass +42 42 | if {1} is named_var: # F632 (fix) +43 43 | pass +44 |-if named_var is {i for i in [1]}: # F632 (fix) + 44 |+if named_var == {i for i in [1]}: # F632 (fix) +45 45 | pass +46 46 | +47 47 | named_var = {1: 1} + +constant_literals.py:48:4: F632 [*] Use `==` to compare constant literals + | +47 | named_var = {1: 1} +48 | if {1: 1} is {1: 1}: # F632 (fix) + | ^^^^^^^^^^^^^^^^ F632 +49 | pass +50 | if named_var is {1: 1}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +45 45 | pass +46 46 | +47 47 | named_var = {1: 1} +48 |-if {1: 1} is {1: 1}: # F632 (fix) + 48 |+if {1: 1} == {1: 1}: # F632 (fix) +49 49 | pass +50 50 | if named_var is {1: 1}: # F632 (fix) +51 51 | pass + +constant_literals.py:50:4: F632 [*] Use `==` to compare constant literals + | +48 | if {1: 1} is {1: 1}: # F632 (fix) +49 | pass +50 | if named_var is {1: 1}: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^ F632 +51 | pass +52 | if {1: 1} is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +47 47 | named_var = {1: 1} +48 48 | if {1: 1} is {1: 1}: # F632 (fix) +49 49 | pass +50 |-if named_var is {1: 1}: # F632 (fix) + 50 |+if named_var == {1: 1}: # F632 (fix) +51 51 | pass +52 52 | if {1: 1} is named_var: # F632 (fix) +53 53 | pass + +constant_literals.py:52:4: F632 [*] Use `==` to compare constant literals + | +50 | if named_var is {1: 1}: # F632 (fix) +51 | pass +52 | if {1: 1} is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^ F632 +53 | pass +54 | if named_var is {1: 1}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +49 49 | pass +50 50 | if named_var is {1: 1}: # F632 (fix) +51 51 | pass +52 |-if {1: 1} is named_var: # F632 (fix) + 52 |+if {1: 1} == named_var: # F632 (fix) +53 53 | pass +54 54 | if named_var is {1: 1}: # F632 (fix) +55 55 | pass + +constant_literals.py:54:4: F632 [*] Use `==` to compare constant literals + | +52 | if {1: 1} is named_var: # F632 (fix) +53 | pass +54 | if named_var is {1: 1}: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^ F632 +55 | pass +56 | if {1: 1} is named_var: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +51 51 | pass +52 52 | if {1: 1} is named_var: # F632 (fix) +53 53 | pass +54 |-if named_var is {1: 1}: # F632 (fix) + 54 |+if named_var == {1: 1}: # F632 (fix) +55 55 | pass +56 56 | if {1: 1} is named_var: # F632 (fix) +57 57 | pass + +constant_literals.py:56:4: F632 [*] Use `==` to compare constant literals + | +54 | if named_var is {1: 1}: # F632 (fix) +55 | pass +56 | if {1: 1} is named_var: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^ F632 +57 | pass +58 | if named_var is {i: 1 for i in [1]}: # F632 (fix) + | + = help: Replace `is` with `==` + +ℹ Safe fix +53 53 | pass +54 54 | if named_var is {1: 1}: # F632 (fix) +55 55 | pass +56 |-if {1: 1} is named_var: # F632 (fix) + 56 |+if {1: 1} == named_var: # F632 (fix) +57 57 | pass +58 58 | if named_var is {i: 1 for i in [1]}: # F632 (fix) +59 59 | pass + +constant_literals.py:58:4: F632 [*] Use `==` to compare constant literals + | +56 | if {1: 1} is named_var: # F632 (fix) +57 | pass +58 | if named_var is {i: 1 for i in [1]}: # F632 (fix) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F632 +59 | pass + | + = help: Replace `is` with `==` + +ℹ Safe fix +55 55 | pass +56 56 | if {1: 1} is named_var: # F632 (fix) +57 57 | pass +58 |-if named_var is {i: 1 for i in [1]}: # F632 (fix) + 58 |+if named_var == {i: 1 for i in [1]}: # F632 (fix) +59 59 | pass +60 60 | +61 61 | ### + + diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs index 1f8e85ad9dd68..fbd0927d9cda9 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/invalid_literal_comparisons.rs @@ -8,6 +8,7 @@ use ruff_python_parser::locate_cmp_ops; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::settings::types::PreviewMode; /// ## What it does /// Checks for `is` and `is not` comparisons against constant literals, like @@ -26,6 +27,12 @@ use crate::checkers::ast::Checker; /// Instead, use `==` and `!=` to compare constant literals, which will compare /// the values of the objects instead of their identities. /// +/// In [preview], this rule will also flag `is` and `is not` comparisons against +/// non-constant literals, like lists, sets, and dictionaries. While such +/// comparisons will not raise a `SyntaxWarning`, they are still likely to be +/// incorrect, as they will compare the identities of the objects instead of +/// their values, which will always evaluate to `False`. +/// /// ## Example /// ```python /// x = 200 @@ -44,6 +51,8 @@ use crate::checkers::ast::Checker; /// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not) /// - [Python documentation: Value comparisons](https://docs.python.org/3/reference/expressions.html#value-comparisons) /// - [_Why does Python log a SyntaxWarning for ‘is’ with literals?_ by Adam Johnson](https://adamj.eu/tech/2020/01/21/why-does-python-3-8-syntaxwarning-for-is-literal/) +/// +/// [preview]: https://docs.astral.sh/ruff/preview/ #[violation] pub struct IsLiteral { cmp_op: IsCmpOp, @@ -81,7 +90,10 @@ pub(crate) fn invalid_literal_comparison( for (index, (op, right)) in ops.iter().zip(comparators).enumerate() { if matches!(op, CmpOp::Is | CmpOp::IsNot) && (helpers::is_constant_non_singleton(left) - || helpers::is_constant_non_singleton(right)) + || helpers::is_constant_non_singleton(right) + || (matches!(checker.settings.preview, PreviewMode::Enabled) + && (helpers::is_mutable_iterable_initializer(left) + || helpers::is_mutable_iterable_initializer(right)))) { let mut diagnostic = Diagnostic::new(IsLiteral { cmp_op: op.into() }, expr.range()); if lazy_located.is_none() { diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 4fd349316c978..2e9a13451c9f8 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -607,6 +607,19 @@ pub const fn is_const_false(expr: &Expr) -> bool { ) } +/// Return `true` if the [`Expr`] is a mutable iterable initializer, like `{}` or `[]`. +pub const fn is_mutable_iterable_initializer(expr: &Expr) -> bool { + matches!( + expr, + Expr::Set(_) + | Expr::SetComp(_) + | Expr::List(_) + | Expr::ListComp(_) + | Expr::Dict(_) + | Expr::DictComp(_) + ) +} + /// Extract the names of all handled exceptions. pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> { let mut handled_exceptions = Vec::new();