diff --git a/Cargo.lock b/Cargo.lock index d7cd647c57..2af0b67c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2287,6 +2287,7 @@ dependencies = [ "ruff_python_literal", "ruff_python_parser", "ruff_source_file", + "ruff_text_size", ] [[package]] diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 540745e5f8..53de3e663b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -180,6 +180,7 @@ impl<'a> Checker<'a> { self.f_string_quote_style().unwrap_or(self.stylist.quote()), self.stylist.line_ending(), ) + .with_locator(self.locator) } /// Returns the appropriate quoting for f-string by reversing the one used outside of diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap index 82e67171d8..7d171f7e04 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap @@ -360,20 +360,6 @@ S608.py:48:12: S608 Possible SQL injection vector through string-based query con 54 | def query38(): | -S608.py:55:12: S608 Possible SQL injection vector through string-based query construction - | -54 | def query38(): -55 | return """ - | ____________^ -56 | | SELECT * -57 | | FROM TABLE -58 | | WHERE var = -59 | | """ + var - | |_____________^ S608 -60 | -61 | def query39(): - | - S608.py:62:12: S608 Possible SQL injection vector through string-based query construction | 61 | def query39(): diff --git a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap index eae2c6f647..2e05a52091 100644 --- a/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bugbear/snapshots/ruff_linter__rules__flake8_bugbear__tests__B006_B006_B008.py.snap @@ -79,14 +79,15 @@ B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument def 75 75 | 76 76 | 77 |-def multiline_arg_wrong(value={ -78 |- -79 |-}): 77 |+def multiline_arg_wrong(value=None): 78 |+ if value is None: - 79 |+ value = {} -80 80 | ... -81 81 | -82 82 | def single_line_func_wrong(value = {}): ... + 79 |+ value = { +78 80 | +79 |-}): + 81 |+ } +80 82 | ... +81 83 | +82 84 | def single_line_func_wrong(value = {}): ... B006_B008.py:82:36: B006 Do not use mutable data structures for argument defaults | @@ -193,7 +194,7 @@ B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument de 102 |-def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]): 102 |+def list_comprehension_also_not_okay(default=None): 103 |+ if default is None: - 104 |+ default = [i ** 2 for i in range(3)] + 104 |+ default = [i**2 for i in range(3)] 103 105 | pass 104 106 | 105 107 | @@ -213,7 +214,7 @@ B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument de 106 |-def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}): 106 |+def dict_comprehension_also_not_okay(default=None): 107 |+ if default is None: - 108 |+ default = {i: i ** 2 for i in range(3)} + 108 |+ default = {i: i**2 for i in range(3)} 107 109 | pass 108 110 | 109 111 | @@ -233,7 +234,7 @@ B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument de 110 |-def set_comprehension_also_not_okay(default={i**2 for i in range(3)}): 110 |+def set_comprehension_also_not_okay(default=None): 111 |+ if default is None: - 112 |+ default = {i ** 2 for i in range(3)} + 112 |+ default = {i**2 for i in range(3)} 111 113 | pass 112 114 | 113 115 | @@ -486,10 +487,12 @@ B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument de 302 |+def single_line_func_wrong(value: dict[str, str] = None): 305 303 | """Docstring""" 304 |+ if value is None: - 305 |+ value = {} -306 306 | -307 307 | -308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \ + 305 |+ value = { + 306 |+ # This is a comment + 307 |+ } +306 308 | +307 309 | +308 310 | def single_line_func_wrong(value: dict[str, str] = {}) \ B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults | diff --git a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 8c95a77556..9e2e6933dc 100644 --- a/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff_linter/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -195,7 +195,10 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { }); let bool_op = node; diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( - checker.generator().expr(&bool_op), + checker + .generator() + .with_locator(checker.locator()) + .expr(&bool_op), expr.range(), ))); checker.diagnostics.push(diagnostic); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap index d3ba1b4f13..624cf32871 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.py.snap @@ -382,7 +382,9 @@ PYI016.py:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` 46 46 | # Shouldn't emit if in new parent type 47 47 | field16: int | dict[int, str] # OK -PYI016.py:57:5: PYI016 Duplicate union member `set[int]` +PYI016.py:57:5: PYI016 Duplicate union member `set[ + int # bar + ]` | 55 | int # foo 56 | ], @@ -393,7 +395,9 @@ PYI016.py:57:5: PYI016 Duplicate union member `set[int]` | |_____^ PYI016 60 | ] # Error, newline and comment will not be emitted in message | - = help: Remove duplicate union member `set[int]` + = help: Remove duplicate union member `set[ + int # bar + ]` PYI016.py:63:28: PYI016 Duplicate union member `int` | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap index 647791e92b..a44187405f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap +++ b/crates/ruff_linter/src/rules/flake8_pyi/snapshots/ruff_linter__rules__flake8_pyi__tests__PYI016_PYI016.pyi.snap @@ -382,7 +382,9 @@ PYI016.pyi:44:30: PYI016 [*] Duplicate union member `typing.Literal[1]` 46 46 | # Shouldn't emit if in new parent type 47 47 | field16: int | dict[int, str] # OK -PYI016.pyi:57:5: PYI016 Duplicate union member `set[int]` +PYI016.pyi:57:5: PYI016 Duplicate union member `set[ + int # bar + ]` | 55 | int # foo 56 | ], @@ -393,7 +395,9 @@ PYI016.pyi:57:5: PYI016 Duplicate union member `set[int]` | |_____^ PYI016 60 | ] # Error, newline and comment will not be emitted in message | - = help: Remove duplicate union member `set[int]` + = help: Remove duplicate union member `set[ + int # bar + ]` PYI016.pyi:63:28: PYI016 Duplicate union member `int` | diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap index 572c08916f..082e1c7c04 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT009.snap @@ -498,7 +498,7 @@ PT009.py:73:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser 71 71 | 72 72 | def test_assert_regex(self): 73 |- self.assertRegex("abc", r"def") # Error - 73 |+ assert re.search("def", "abc") # Error + 73 |+ assert re.search(r"def", "abc") # Error 74 74 | 75 75 | def test_assert_not_regex(self): 76 76 | self.assertNotRegex("abc", r"abc") # Error @@ -518,7 +518,7 @@ PT009.py:76:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser 74 74 | 75 75 | def test_assert_not_regex(self): 76 |- self.assertNotRegex("abc", r"abc") # Error - 76 |+ assert not re.search("abc", "abc") # Error + 76 |+ assert not re.search(r"abc", "abc") # Error 77 77 | 78 78 | def test_assert_regexp_matches(self): 79 79 | self.assertRegexpMatches("abc", r"def") # Error @@ -538,7 +538,7 @@ PT009.py:79:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser 77 77 | 78 78 | def test_assert_regexp_matches(self): 79 |- self.assertRegexpMatches("abc", r"def") # Error - 79 |+ assert re.search("def", "abc") # Error + 79 |+ assert re.search(r"def", "abc") # Error 80 80 | 81 81 | def test_assert_not_regexp_matches(self): 82 82 | self.assertNotRegex("abc", r"abc") # Error @@ -558,7 +558,7 @@ PT009.py:82:9: PT009 [*] Use a regular `assert` instead of unittest-style `asser 80 80 | 81 81 | def test_assert_not_regexp_matches(self): 82 |- self.assertNotRegex("abc", r"abc") # Error - 82 |+ assert not re.search("abc", "abc") # Error + 82 |+ assert not re.search(r"abc", "abc") # Error 83 83 | 84 84 | def test_fail_if(self): 85 85 | self.failIf("abc") # Error @@ -658,6 +658,7 @@ PT009.py:98:2: PT009 [*] Use a regular `assert` instead of unittest-style `asser 98 |-(self.assertTrue( 99 |- "piAx_piAy_beta[r][x][y] = {17}".format( 100 |- self.model.piAx_piAy_beta[r][x][y]))) - 98 |+assert "piAx_piAy_beta[r][x][y] = {17}".format(self.model.piAx_piAy_beta[r][x][y]) + 98 |+assert "piAx_piAy_beta[r][x][y] = {17}".format( + 99 |+ self.model.piAx_piAy_beta[r][x][y]) diff --git a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap index ddf4598e13..f25907886c 100644 --- a/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap +++ b/crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM222_SIM222.py.snap @@ -534,7 +534,7 @@ SIM222.py:85:6: SIM222 [*] Use `True` instead of `... or True` 87 87 | a or (1,) or True or (2,) # SIM222 88 88 | -SIM222.py:87:6: SIM222 [*] Use `(1,)` instead of `(1,) or ...` +SIM222.py:87:6: SIM222 [*] Use `((1,))` instead of `((1,)) or ...` | 85 | a or tuple(()) or True # SIM222 86 | @@ -543,14 +543,14 @@ SIM222.py:87:6: SIM222 [*] Use `(1,)` instead of `(1,) or ...` 88 | 89 | a or tuple((1,)) or True or tuple((2,)) # SIM222 | - = help: Replace with `(1,)` + = help: Replace with `((1,))` ℹ Unsafe fix 84 84 | 85 85 | a or tuple(()) or True # SIM222 86 86 | 87 |-a or (1,) or True or (2,) # SIM222 - 87 |+a or (1,) # SIM222 + 87 |+a or ((1,)) # SIM222 88 88 | 89 89 | a or tuple((1,)) or True or tuple((2,)) # SIM222 90 90 | @@ -1006,39 +1006,39 @@ SIM222.py:153:11: SIM222 [*] Use `[1]` instead of `[1] or ...` 155 155 | 156 156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099 -SIM222.py:157:30: SIM222 [*] Use `(int, int, int)` instead of `(int, int, int) or ...` +SIM222.py:157:30: SIM222 [*] Use `((int, int, int))` instead of `((int, int, int)) or ...` | 156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099 157 | def secondToTime(s0: int) -> (int, int, int) or str: | ^^^^^^^^^^^^^^^^^^^^^^ SIM222 158 | m, s = divmod(s0, 60) | - = help: Replace with `(int, int, int)` + = help: Replace with `((int, int, int))` ℹ Unsafe fix 154 154 | pass 155 155 | 156 156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099 157 |-def secondToTime(s0: int) -> (int, int, int) or str: - 157 |+def secondToTime(s0: int) -> (int, int, int): + 157 |+def secondToTime(s0: int) -> ((int, int, int)): 158 158 | m, s = divmod(s0, 60) 159 159 | 160 160 | -SIM222.py:161:31: SIM222 [*] Use `(int, int, int)` instead of `(int, int, int) or ...` +SIM222.py:161:31: SIM222 [*] Use `((int, int, int))` instead of `((int, int, int)) or ...` | 161 | def secondToTime(s0: int) -> ((int, int, int) or str): | ^^^^^^^^^^^^^^^^^^^^^^ SIM222 162 | m, s = divmod(s0, 60) | - = help: Replace with `(int, int, int)` + = help: Replace with `((int, int, int))` ℹ Unsafe fix 158 158 | m, s = divmod(s0, 60) 159 159 | 160 160 | 161 |-def secondToTime(s0: int) -> ((int, int, int) or str): - 161 |+def secondToTime(s0: int) -> ((int, int, int)): + 161 |+def secondToTime(s0: int) -> (((int, int, int))): 162 162 | m, s = divmod(s0, 60) 163 163 | 164 164 | diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap index ee9c609b64..1755f8243c 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__PLW2901_redefined_loop_name.py.snap @@ -241,7 +241,7 @@ redefined_loop_name.py:178:5: PLW2901 `for` loop variable `a.i` overwritten by a 180 | a.i = 2 # error | -redefined_loop_name.py:180:5: PLW2901 `for` loop variable `a.i` overwritten by assignment target +redefined_loop_name.py:180:5: PLW2901 `for` loop variable `a. i` overwritten by assignment target | 178 | a. i = 2 # error 179 | for a. i in []: diff --git a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__allow_magic_value_types.snap b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__allow_magic_value_types.snap index d810fbb1f5..aab7558fe8 100644 --- a/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__allow_magic_value_types.snap +++ b/crates/ruff_linter/src/rules/pylint/snapshots/ruff_linter__rules__pylint__tests__allow_magic_value_types.snap @@ -10,7 +10,7 @@ magic_value_comparison.py:59:22: PLR2004 Magic value used in comparison, conside 60 | pass | -magic_value_comparison.py:65:21: PLR2004 Magic value used in comparison, consider replacing 3.141592653589793 with a constant variable +magic_value_comparison.py:65:21: PLR2004 Magic value used in comparison, consider replacing 3.141592653589793238 with a constant variable | 63 | pi_estimation = 3.14 64 | diff --git a/crates/ruff_python_codegen/Cargo.toml b/crates/ruff_python_codegen/Cargo.toml index 6c55754be3..f7b73ed9e8 100644 --- a/crates/ruff_python_codegen/Cargo.toml +++ b/crates/ruff_python_codegen/Cargo.toml @@ -17,6 +17,7 @@ ruff_python_ast = { path = "../ruff_python_ast" } ruff_python_literal = { path = "../ruff_python_literal" } ruff_python_parser = { path = "../ruff_python_parser" } ruff_source_file = { path = "../ruff_source_file" } +ruff_text_size = { path = "../ruff_text_size" } once_cell = { workspace = true } diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index 77cdcd3252..e19b4a7cbb 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -10,7 +10,8 @@ use ruff_python_ast::{ }; use ruff_python_ast::{ParameterWithDefault, TypeParams}; use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape}; -use ruff_source_file::LineEnding; +use ruff_source_file::{LineEnding, Locator}; +use ruff_text_size::{Ranged, TextRange}; use super::stylist::{Indentation, Quote, Stylist}; @@ -61,6 +62,11 @@ mod precedence { pub(crate) const MAX: u8 = 63; } +pub struct Verbatim<'a> { + locator: &'a Locator<'a>, + nodes: Vec, +} + pub struct Generator<'a> { /// The indentation style to use. indent: &'a Indentation, @@ -72,6 +78,7 @@ pub struct Generator<'a> { indent_depth: usize, num_newlines: usize, initial: bool, + locator: Option<&'a Locator<'a>>, } impl<'a> From<&'a Stylist<'a>> for Generator<'a> { @@ -84,6 +91,7 @@ impl<'a> From<&'a Stylist<'a>> for Generator<'a> { indent_depth: 0, num_newlines: 0, initial: true, + locator: None, } } } @@ -100,6 +108,15 @@ impl<'a> Generator<'a> { indent_depth: 0, num_newlines: 0, initial: true, + locator: None, + } + } + + #[must_use] + pub fn with_locator(self, locator: &'a Locator<'a>) -> Self { + Self { + locator: Some(locator), + ..self } } @@ -796,6 +813,14 @@ impl<'a> Generator<'a> { ret }}; } + + if let Some(locator) = &self.locator { + if !ast.range().is_empty() { + self.p(locator.slice(ast)); + return; + } + } + match ast { Expr::BoolOp(ast::ExprBoolOp { op, diff --git a/foo.py b/foo.py new file mode 100644 index 0000000000..2a5b80347d --- /dev/null +++ b/foo.py @@ -0,0 +1,2 @@ +if x.startswith(("a", "b")) or re.match(r"a\.b", x): + pass