From af7051b976e9d4a266de0d600f2f06908f5d75f6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 20:18:33 -0400 Subject: [PATCH 01/27] Include BaseException in B017 rule (#5466) Closes #5462. --- .../test/fixtures/flake8_bugbear/B017.py | 7 + crates/ruff/src/checkers/ast/mod.rs | 2 +- .../rules/assert_raises_exception.rs | 122 +++++++++++------- ...__flake8_bugbear__tests__B017_B017.py.snap | 45 ++++--- .../pyflakes/rules/yield_outside_function.rs | 2 +- 5 files changed, 111 insertions(+), 67 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py index 43f1b986e9..917a848ba1 100644 --- a/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py +++ b/crates/ruff/resources/test/fixtures/flake8_bugbear/B017.py @@ -23,6 +23,10 @@ class Foobar(unittest.TestCase): with self.assertRaises(Exception): raise Exception("Evil I say!") + def also_evil_raises(self) -> None: + with self.assertRaises(BaseException): + raise Exception("Evil I say!") + def context_manager_raises(self) -> None: with self.assertRaises(Exception) as ex: raise Exception("Context manager is good") @@ -41,6 +45,9 @@ def test_pytest_raises(): with pytest.raises(Exception): raise ValueError("Hello") + with pytest.raises(Exception), pytest.raises(ValueError): + raise ValueError("Hello") + with pytest.raises(Exception, "hello"): raise ValueError("This is fine") diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 012d999ea3..bd408fb22a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1409,7 +1409,7 @@ where Stmt::With(ast::StmtWith { items, body, .. }) | Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => { if self.enabled(Rule::AssertRaisesException) { - flake8_bugbear::rules::assert_raises_exception(self, stmt, items); + flake8_bugbear::rules::assert_raises_exception(self, items); } if self.enabled(Rule::PytestRaisesWithMultipleStatements) { flake8_pytest_style::rules::complex_raises(self, stmt, items, body); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index 7dfb6c6d91..d7a3994e76 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -1,22 +1,20 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt, WithItem}; +use std::fmt; + +use rustpython_parser::ast::{self, Expr, Ranged, WithItem}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum AssertionKind { - AssertRaises, - PytestRaises, -} - /// ## What it does -/// Checks for `self.assertRaises(Exception)` or `pytest.raises(Exception)`. +/// Checks for `assertRaises` and `pytest.raises` context managers that catch +/// `Exception` or `BaseException`. /// /// ## Why is this bad? /// These forms catch every `Exception`, which can lead to tests passing even -/// if, e.g., the code being tested is never executed due to a typo. +/// if, e.g., the code under consideration raises a `SyntaxError` or +/// `IndentationError`. /// /// Either assert for a more specific exception (builtin or custom), or use /// `assertRaisesRegex` or `pytest.raises(..., match=)` respectively. @@ -32,51 +30,76 @@ pub(crate) enum AssertionKind { /// ``` #[violation] pub struct AssertRaisesException { - kind: AssertionKind, + assertion: AssertionKind, + exception: ExceptionKind, } impl Violation for AssertRaisesException { #[derive_message_formats] fn message(&self) -> String { - match self.kind { - AssertionKind::AssertRaises => { - format!("`assertRaises(Exception)` should be considered evil") - } - AssertionKind::PytestRaises => { - format!("`pytest.raises(Exception)` should be considered evil") - } + let AssertRaisesException { + assertion, + exception, + } = self; + format!("`{assertion}({exception})` should be considered evil") + } +} + +#[derive(Debug, PartialEq, Eq)] +enum AssertionKind { + AssertRaises, + PytestRaises, +} + +impl fmt::Display for AssertionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + AssertionKind::AssertRaises => fmt.write_str("assertRaises"), + AssertionKind::PytestRaises => fmt.write_str("pytest.raises"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ExceptionKind { + BaseException, + Exception, +} + +impl fmt::Display for ExceptionKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + ExceptionKind::BaseException => fmt.write_str("BaseException"), + ExceptionKind::Exception => fmt.write_str("Exception"), } } } /// B017 -pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: &[WithItem]) { - let Some(item) = items.first() else { - return; - }; - let item_context = &item.context_expr; - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item_context else { - return; - }; - if args.len() != 1 { - return; - } - if item.optional_vars.is_some() { - return; - } +pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) { + for item in items { + let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item.context_expr else { + return; + }; + if args.len() != 1 { + return; + } + if item.optional_vars.is_some() { + return; + } - if !checker - .semantic() - .resolve_call_path(args.first().unwrap()) - .map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "Exception"]) - }) - { - return; - } + let Some(exception) = checker + .semantic() + .resolve_call_path(args.first().unwrap()) + .and_then(|call_path| { + match call_path.as_slice() { + ["", "Exception"] => Some(ExceptionKind::Exception), + ["", "BaseException"] => Some(ExceptionKind::BaseException), + _ => None, + } + }) else { return; }; - let kind = { - if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") + let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") { AssertionKind::AssertRaises } else if checker @@ -92,11 +115,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, stmt: &Stmt, items: AssertionKind::PytestRaises } else { return; - } - }; + }; - checker.diagnostics.push(Diagnostic::new( - AssertRaisesException { kind }, - stmt.range(), - )); + checker.diagnostics.push(Diagnostic::new( + AssertRaisesException { + assertion, + exception, + }, + item.range(), + )); + } } diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap index f46fd7d74e..d6a332dfc2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B017_B017.py.snap @@ -1,27 +1,38 @@ --- source: crates/ruff/src/rules/flake8_bugbear/mod.rs --- -B017.py:23:9: B017 `assertRaises(Exception)` should be considered evil +B017.py:23:14: B017 `assertRaises(Exception)` should be considered evil | -21 | class Foobar(unittest.TestCase): -22 | def evil_raises(self) -> None: -23 | with self.assertRaises(Exception): - | _________^ -24 | | raise Exception("Evil I say!") - | |__________________________________________^ B017 -25 | -26 | def context_manager_raises(self) -> None: +21 | class Foobar(unittest.TestCase): +22 | def evil_raises(self) -> None: +23 | with self.assertRaises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +24 | raise Exception("Evil I say!") | -B017.py:41:5: B017 `pytest.raises(Exception)` should be considered evil +B017.py:27:14: B017 `assertRaises(BaseException)` should be considered evil | -40 | def test_pytest_raises(): -41 | with pytest.raises(Exception): - | _____^ -42 | | raise ValueError("Hello") - | |_________________________________^ B017 -43 | -44 | with pytest.raises(Exception, "hello"): +26 | def also_evil_raises(self) -> None: +27 | with self.assertRaises(BaseException): + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B017 +28 | raise Exception("Evil I say!") + | + +B017.py:45:10: B017 `pytest.raises(Exception)` should be considered evil + | +44 | def test_pytest_raises(): +45 | with pytest.raises(Exception): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +46 | raise ValueError("Hello") + | + +B017.py:48:10: B017 `pytest.raises(Exception)` should be considered evil + | +46 | raise ValueError("Hello") +47 | +48 | with pytest.raises(Exception), pytest.raises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^^^^ B017 +49 | raise ValueError("Hello") | diff --git a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs index 54fab4be14..f57f092ce5 100644 --- a/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/yield_outside_function.rs @@ -9,7 +9,7 @@ use ruff_python_semantic::ScopeKind; use crate::checkers::ast::Checker; #[derive(Debug, PartialEq, Eq)] -pub(crate) enum DeferralKeyword { +enum DeferralKeyword { Yield, YieldFrom, Await, From b32d1e8d78a755de254785ad6bbfefa37f8fcbb2 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 20:29:45 -0400 Subject: [PATCH 02/27] Detect consecutive, non-newline-delimited NumPy sections (#5467) ## Summary Given a docstring like: ```py def f(a: int, b: int) -> int: """Showcase function. Parameters ---------- a : int _description_ b : int _description_ Returns ------- int _description """ ``` We were failing to identify `Returns` as a section, because the previous line was neither empty nor ended with punctuation. This was causing a false negative, where by we weren't flagging a missing line before `Returns`. So, the very reason for the rule (no blank line) was causing us to fail to catch it. Note that, we did have a test case for this, which was working properly: ```py def f() -> int: """Showcase function. Parameters ---------- Returns ------- """ ``` ...because the line before `Returns` "ends in a punctuation mark" (`-`). Closes #5442. --- .../test/fixtures/pydocstyle/D410.py | 25 ++++++++ crates/ruff/src/checkers/ast/mod.rs | 1 + crates/ruff/src/docstrings/sections.rs | 57 ++++++++++++------ crates/ruff/src/rules/pydocstyle/mod.rs | 1 + ...ules__pydocstyle__tests__D410_D410.py.snap | 59 +++++++++++++++++++ 5 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/pydocstyle/D410.py create mode 100644 crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap diff --git a/crates/ruff/resources/test/fixtures/pydocstyle/D410.py b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py new file mode 100644 index 0000000000..b9aec80568 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pydocstyle/D410.py @@ -0,0 +1,25 @@ +def f(a: int, b: int) -> int: + """Showcase function. + + Parameters + ---------- + a : int + _description_ + b : int + _description_ + Returns + ------- + int + _description + """ + return b - a + + +def f() -> int: + """Showcase function. + + Parameters + ---------- + Returns + ------- + """ diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index bd408fb22a..d722ee2a77 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -36,6 +36,7 @@ use crate::importer::Importer; use crate::noqa::NoqaMapping; use crate::registry::Rule; use crate::rules::flake8_builtins::helpers::AnyShadowing; + use crate::rules::{ airflow, flake8_2020, flake8_annotations, flake8_async, flake8_bandit, flake8_blind_except, flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, diff --git a/crates/ruff/src/docstrings/sections.rs b/crates/ruff/src/docstrings/sections.rs index 48f38e606d..6bdca985f4 100644 --- a/crates/ruff/src/docstrings/sections.rs +++ b/crates/ruff/src/docstrings/sections.rs @@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words}; use ruff_text_size::{TextLen, TextRange, TextSize}; use strum_macros::EnumIter; -use ruff_python_whitespace::{UniversalNewlineIterator, UniversalNewlines}; +use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines}; use crate::docstrings::styles::SectionStyle; use crate::docstrings::{Docstring, DocstringBody}; @@ -144,15 +144,13 @@ impl<'a> SectionContexts<'a> { let mut contexts = Vec::new(); let mut last: Option = None; - let mut previous_line = None; - for line in contents.universal_newlines() { - if previous_line.is_none() { - // skip the first line - previous_line = Some(line.as_str()); - continue; - } + let mut lines = contents.universal_newlines().peekable(); + // Skip the first line, which is the summary. + let mut previous_line = lines.next(); + + while let Some(line) = lines.next() { if let Some(section_kind) = suspected_as_section(&line, style) { let indent = leading_space(&line); let section_name = leading_words(&line); @@ -162,7 +160,8 @@ impl<'a> SectionContexts<'a> { if is_docstring_section( &line, section_name_range, - previous_line.unwrap_or_default(), + previous_line.as_ref(), + lines.peek(), ) { if let Some(mut last) = last.take() { last.range = TextRange::new(last.range.start(), line.start()); @@ -178,7 +177,7 @@ impl<'a> SectionContexts<'a> { } } - previous_line = Some(line.as_str()); + previous_line = Some(line); } if let Some(mut last) = last.take() { @@ -388,7 +387,13 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option } /// Check if the suspected context is really a section header. -fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool { +fn is_docstring_section( + line: &Line, + section_name_range: TextRange, + previous_line: Option<&Line>, + next_line: Option<&Line>, +) -> bool { + // Determine whether the current line looks like a section header, e.g., "Args:". let section_name_suffix = line[usize::from(section_name_range.end())..].trim(); let this_looks_like_a_section_name = section_name_suffix == ":" || section_name_suffix.is_empty(); @@ -396,13 +401,29 @@ fn is_docstring_section(line: &str, section_name_range: TextRange, previous_line return false; } - let prev_line = previous_lines.trim(); - let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] - .into_iter() - .any(|char| prev_line.ends_with(char)); - let prev_line_looks_like_end_of_paragraph = - prev_line_ends_with_punctuation || prev_line.is_empty(); - if !prev_line_looks_like_end_of_paragraph { + // Determine whether the next line is an underline, e.g., "-----". + let next_line_is_underline = next_line.map_or(false, |next_line| { + let next_line = next_line.trim(); + if next_line.is_empty() { + false + } else { + let next_line_is_underline = next_line.chars().all(|char| matches!(char, '-' | '=')); + next_line_is_underline + } + }); + if next_line_is_underline { + return true; + } + + // Determine whether the previous line looks like the end of a paragraph. + let previous_line_looks_like_end_of_paragraph = previous_line.map_or(true, |previous_line| { + let previous_line = previous_line.trim(); + let previous_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')'] + .into_iter() + .any(|char| previous_line.ends_with(char)); + previous_line_ends_with_punctuation || previous_line.is_empty() + }); + if !previous_line_looks_like_end_of_paragraph { return false; } diff --git a/crates/ruff/src/rules/pydocstyle/mod.rs b/crates/ruff/src/rules/pydocstyle/mod.rs index ca0035e418..41c6a1fc6f 100644 --- a/crates/ruff/src/rules/pydocstyle/mod.rs +++ b/crates/ruff/src/rules/pydocstyle/mod.rs @@ -51,6 +51,7 @@ mod tests { #[test_case(Rule::EmptyDocstring, Path::new("D.py"))] #[test_case(Rule::EmptyDocstringSection, Path::new("sections.py"))] #[test_case(Rule::NonImperativeMood, Path::new("D401.py"))] + #[test_case(Rule::NoBlankLineAfterSection, Path::new("D410.py"))] #[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"))] #[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"))] #[test_case(Rule::UndocumentedPublicClass, Path::new("D.py"))] diff --git a/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap new file mode 100644 index 0000000000..0ce1725147 --- /dev/null +++ b/crates/ruff/src/rules/pydocstyle/snapshots/ruff__rules__pydocstyle__tests__D410_D410.py.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff/src/rules/pydocstyle/mod.rs +--- +D410.py:2:5: D410 [*] Missing blank line after section ("Parameters") + | + 1 | def f(a: int, b: int) -> int: + 2 | """Showcase function. + | _____^ + 3 | | + 4 | | Parameters + 5 | | ---------- + 6 | | a : int + 7 | | _description_ + 8 | | b : int + 9 | | _description_ +10 | | Returns +11 | | ------- +12 | | int +13 | | _description +14 | | """ + | |_______^ D410 +15 | return b - a + | + = help: Add blank line after "Parameters" + +ℹ Fix +7 7 | _description_ +8 8 | b : int +9 9 | _description_ + 10 |+ +10 11 | Returns +11 12 | ------- +12 13 | int + +D410.py:19:5: D410 [*] Missing blank line after section ("Parameters") + | +18 | def f() -> int: +19 | """Showcase function. + | _____^ +20 | | +21 | | Parameters +22 | | ---------- +23 | | Returns +24 | | ------- +25 | | """ + | |_______^ D410 + | + = help: Add blank line after "Parameters" + +ℹ Fix +20 20 | +21 21 | Parameters +22 22 | ---------- + 23 |+ +23 24 | Returns +24 25 | ------- +25 26 | """ + + From d0b2fffb8748e3c785b53620bff1153694f8239f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 20:50:14 -0400 Subject: [PATCH 03/27] [`numpy`] Add `numpy-deprecated-function` (NPY003) (#5468) ## Summary Closes #5456. --- .../resources/test/fixtures/numpy/NPY002.py | 3 + .../resources/test/fixtures/numpy/NPY003.py | 15 + crates/ruff/src/checkers/ast/mod.rs | 8 +- crates/ruff/src/codes.rs | 1 + crates/ruff/src/rules/numpy/mod.rs | 1 + .../rules/numpy/rules/deprecated_function.rs | 97 +++ ...umpy_legacy_random.rs => legacy_random.rs} | 2 +- crates/ruff/src/rules/numpy/rules/mod.rs | 6 +- ...__numpy-deprecated-function_NPY003.py.snap | 98 +++ ..._tests__numpy-legacy-random_NPY002.py.snap | 589 +++++++++--------- 10 files changed, 521 insertions(+), 299 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/numpy/NPY003.py create mode 100644 crates/ruff/src/rules/numpy/rules/deprecated_function.rs rename crates/ruff/src/rules/numpy/rules/{numpy_legacy_random.rs => legacy_random.rs} (98%) create mode 100644 crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY002.py b/crates/ruff/resources/test/fixtures/numpy/NPY002.py index d0e2274e6d..129b270cab 100644 --- a/crates/ruff/resources/test/fixtures/numpy/NPY002.py +++ b/crates/ruff/resources/test/fixtures/numpy/NPY002.py @@ -1,5 +1,6 @@ # Do this (new version) from numpy.random import default_rng + rng = default_rng() vals = rng.standard_normal(10) more_vals = rng.standard_normal(10) @@ -7,11 +8,13 @@ numbers = rng.integers(high, size=5) # instead of this (legacy version) from numpy import random + vals = random.standard_normal(10) more_vals = random.standard_normal(10) numbers = random.integers(high, size=5) import numpy + numpy.random.seed() numpy.random.get_state() numpy.random.set_state() diff --git a/crates/ruff/resources/test/fixtures/numpy/NPY003.py b/crates/ruff/resources/test/fixtures/numpy/NPY003.py new file mode 100644 index 0000000000..6d6f369771 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/numpy/NPY003.py @@ -0,0 +1,15 @@ +import numpy as np + +np.round_(np.random.rand(5, 5), 2) +np.product(np.random.rand(5, 5)) +np.cumproduct(np.random.rand(5, 5)) +np.sometrue(np.random.rand(5, 5)) +np.alltrue(np.random.rand(5, 5)) + +from numpy import round_, product, cumproduct, sometrue, alltrue + +round_(np.random.rand(5, 5), 2) +product(np.random.rand(5, 5)) +cumproduct(np.random.rand(5, 5)) +sometrue(np.random.rand(5, 5)) +alltrue(np.random.rand(5, 5)) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d722ee2a77..30c5c539b0 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2194,6 +2194,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.is_stub { if self.enabled(Rule::CollectionsNamedTuple) { flake8_pyi::rules::collections_named_tuple(self, expr); @@ -2316,6 +2319,9 @@ where if self.enabled(Rule::NumpyDeprecatedTypeAlias) { numpy::rules::deprecated_type_alias(self, expr); } + if self.enabled(Rule::NumpyDeprecatedFunction) { + numpy::rules::deprecated_function(self, expr); + } if self.enabled(Rule::DeprecatedMockImport) { pyupgrade::rules::deprecated_mock_attribute(self, expr); } @@ -2870,7 +2876,7 @@ where flake8_use_pathlib::rules::replaceable_by_pathlib(self, func); } if self.enabled(Rule::NumpyLegacyRandom) { - numpy::rules::numpy_legacy_random(self, func); + numpy::rules::legacy_random(self, func); } if self.any_enabled(&[ Rule::LoggingStringFormat, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 15b334e884..cc25933e8c 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -742,6 +742,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // numpy (Numpy, "001") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedTypeAlias), (Numpy, "002") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyLegacyRandom), + (Numpy, "003") => (RuleGroup::Unspecified, rules::numpy::rules::NumpyDeprecatedFunction), // ruff (Ruff, "001") => (RuleGroup::Unspecified, rules::ruff::rules::AmbiguousUnicodeCharacterString), diff --git a/crates/ruff/src/rules/numpy/mod.rs b/crates/ruff/src/rules/numpy/mod.rs index 37ddc17ffb..2bdb951dac 100644 --- a/crates/ruff/src/rules/numpy/mod.rs +++ b/crates/ruff/src/rules/numpy/mod.rs @@ -15,6 +15,7 @@ mod tests { #[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"))] #[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"))] + #[test_case(Rule::NumpyDeprecatedFunction, Path::new("NPY003.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/numpy/rules/deprecated_function.rs b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs new file mode 100644 index 0000000000..3fb2774cfb --- /dev/null +++ b/crates/ruff/src/rules/numpy/rules/deprecated_function.rs @@ -0,0 +1,97 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for uses of deprecated NumPy functions. +/// +/// ## Why is this bad? +/// When NumPy functions are deprecated, they are usually replaced with +/// newer, more efficient versions, or with functions that are more +/// consistent with the rest of the NumPy API. +/// +/// Prefer newer APIs over deprecated ones. +/// +/// ## Examples +/// ```python +/// import numpy as np +/// +/// np.alltrue([True, False]) +/// ``` +/// +/// Use instead: +/// ```python +/// import numpy as np +/// +/// np.all([True, False]) +/// ``` +#[violation] +pub struct NumpyDeprecatedFunction { + existing: String, + replacement: String, +} + +impl Violation for NumpyDeprecatedFunction { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let NumpyDeprecatedFunction { + existing, + replacement, + } = self; + format!("`np.{existing}` is deprecated; use `np.{replacement}` instead") + } + + fn autofix_title(&self) -> Option { + let NumpyDeprecatedFunction { replacement, .. } = self; + Some(format!("Replace with `np.{replacement}`")) + } +} + +/// NPY003 +pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) { + if let Some((existing, replacement)) = + checker + .semantic() + .resolve_call_path(expr) + .and_then(|call_path| match call_path.as_slice() { + ["numpy", "round_"] => Some(("round_", "round")), + ["numpy", "product"] => Some(("product", "prod")), + ["numpy", "cumproduct"] => Some(("cumproduct", "cumprod")), + ["numpy", "sometrue"] => Some(("sometrue", "any")), + ["numpy", "alltrue"] => Some(("alltrue", "all")), + _ => None, + }) + { + let mut diagnostic = Diagnostic::new( + NumpyDeprecatedFunction { + existing: existing.to_string(), + replacement: replacement.to_string(), + }, + expr.range(), + ); + if checker.patch(diagnostic.kind.rule()) { + match expr { + Expr::Name(_) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + expr.range(), + ))); + } + Expr::Attribute(ast::ExprAttribute { attr, .. }) => { + diagnostic.set_fix(Fix::suggested(Edit::range_replacement( + replacement.to_string(), + attr.range(), + ))); + } + _ => {} + } + } + checker.diagnostics.push(diagnostic); + } +} diff --git a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs b/crates/ruff/src/rules/numpy/rules/legacy_random.rs similarity index 98% rename from crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs rename to crates/ruff/src/rules/numpy/rules/legacy_random.rs index 46b66a65e4..cdcd68f149 100644 --- a/crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs +++ b/crates/ruff/src/rules/numpy/rules/legacy_random.rs @@ -57,7 +57,7 @@ impl Violation for NumpyLegacyRandom { } /// NPY002 -pub(crate) fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) { +pub(crate) fn legacy_random(checker: &mut Checker, expr: &Expr) { if let Some(method_name) = checker .semantic() .resolve_call_path(expr) diff --git a/crates/ruff/src/rules/numpy/rules/mod.rs b/crates/ruff/src/rules/numpy/rules/mod.rs index 25c3f2fb17..7c46515e76 100644 --- a/crates/ruff/src/rules/numpy/rules/mod.rs +++ b/crates/ruff/src/rules/numpy/rules/mod.rs @@ -1,5 +1,7 @@ +pub(crate) use deprecated_function::*; pub(crate) use deprecated_type_alias::*; -pub(crate) use numpy_legacy_random::*; +pub(crate) use legacy_random::*; +mod deprecated_function; mod deprecated_type_alias; -mod numpy_legacy_random; +mod legacy_random; diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap new file mode 100644 index 0000000000..6821b3e2d2 --- /dev/null +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap @@ -0,0 +1,98 @@ +--- +source: crates/ruff/src/rules/numpy/mod.rs +--- +NPY003.py:3:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | +1 | import numpy as np +2 | +3 | np.round_(np.random.rand(5, 5), 2) + | ^^^^^^^^^ NPY003 +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 |-np.round_(np.random.rand(5, 5), 2) + 3 |+np.round(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) + +NPY003.py:4:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +1 1 | import numpy as np +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 |-np.product(np.random.rand(5, 5)) + 4 |+np.prod(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) + +NPY003.py:5:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +3 | np.round_(np.random.rand(5, 5), 2) +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^^^^ NPY003 +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +2 2 | +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 |-np.cumproduct(np.random.rand(5, 5)) + 5 |+np.cumprod(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) + +NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +4 | np.product(np.random.rand(5, 5)) +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) + | ^^^^^^^^^^^ NPY003 +7 | np.alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +3 3 | np.round_(np.random.rand(5, 5), 2) +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 |-np.sometrue(np.random.rand(5, 5)) + 6 |+np.any(np.random.rand(5, 5)) +7 7 | np.alltrue(np.random.rand(5, 5)) + +NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +5 | np.cumproduct(np.random.rand(5, 5)) +6 | np.sometrue(np.random.rand(5, 5)) +7 | np.alltrue(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 + | + = help: Replace with `np.all` + +ℹ Suggested fix +4 4 | np.product(np.random.rand(5, 5)) +5 5 | np.cumproduct(np.random.rand(5, 5)) +6 6 | np.sometrue(np.random.rand(5, 5)) +7 |-np.alltrue(np.random.rand(5, 5)) + 7 |+np.all(np.random.rand(5, 5)) + + diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap index 38890264a4..c06c12bee6 100644 --- a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-legacy-random_NPY002.py.snap @@ -1,498 +1,497 @@ --- source: crates/ruff/src/rules/numpy/mod.rs --- -NPY002.py:10:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:12:8: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 8 | # instead of this (legacy version) - 9 | from numpy import random -10 | vals = random.standard_normal(10) +10 | from numpy import random +11 | +12 | vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -11 | more_vals = random.standard_normal(10) -12 | numbers = random.integers(high, size=5) +13 | more_vals = random.standard_normal(10) +14 | numbers = random.integers(high, size=5) | -NPY002.py:11:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:13:13: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | - 9 | from numpy import random -10 | vals = random.standard_normal(10) -11 | more_vals = random.standard_normal(10) +12 | vals = random.standard_normal(10) +13 | more_vals = random.standard_normal(10) | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -12 | numbers = random.integers(high, size=5) +14 | numbers = random.integers(high, size=5) | -NPY002.py:15:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` +NPY002.py:18:1: NPY002 Replace legacy `np.random.seed` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() +16 | import numpy +17 | +18 | numpy.random.seed() | ^^^^^^^^^^^^^^^^^ NPY002 -16 | numpy.random.get_state() -17 | numpy.random.set_state() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | -NPY002.py:16:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:19:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -14 | import numpy -15 | numpy.random.seed() -16 | numpy.random.get_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -17 | numpy.random.set_state() -18 | numpy.random.rand() +20 | numpy.random.set_state() +21 | numpy.random.rand() | -NPY002.py:17:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` +NPY002.py:20:1: NPY002 Replace legacy `np.random.set_state` call with `np.random.Generator` | -15 | numpy.random.seed() -16 | numpy.random.get_state() -17 | numpy.random.set_state() +18 | numpy.random.seed() +19 | numpy.random.get_state() +20 | numpy.random.set_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -18 | numpy.random.rand() -19 | numpy.random.randn() +21 | numpy.random.rand() +22 | numpy.random.randn() | -NPY002.py:18:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` +NPY002.py:21:1: NPY002 Replace legacy `np.random.rand` call with `np.random.Generator` | -16 | numpy.random.get_state() -17 | numpy.random.set_state() -18 | numpy.random.rand() +19 | numpy.random.get_state() +20 | numpy.random.set_state() +21 | numpy.random.rand() | ^^^^^^^^^^^^^^^^^ NPY002 -19 | numpy.random.randn() -20 | numpy.random.randint() +22 | numpy.random.randn() +23 | numpy.random.randint() | -NPY002.py:19:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` +NPY002.py:22:1: NPY002 Replace legacy `np.random.randn` call with `np.random.Generator` | -17 | numpy.random.set_state() -18 | numpy.random.rand() -19 | numpy.random.randn() +20 | numpy.random.set_state() +21 | numpy.random.rand() +22 | numpy.random.randn() | ^^^^^^^^^^^^^^^^^^ NPY002 -20 | numpy.random.randint() -21 | numpy.random.random_integers() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | -NPY002.py:20:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` +NPY002.py:23:1: NPY002 Replace legacy `np.random.randint` call with `np.random.Generator` | -18 | numpy.random.rand() -19 | numpy.random.randn() -20 | numpy.random.randint() +21 | numpy.random.rand() +22 | numpy.random.randn() +23 | numpy.random.randint() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | -NPY002.py:21:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` +NPY002.py:24:1: NPY002 Replace legacy `np.random.random_integers` call with `np.random.Generator` | -19 | numpy.random.randn() -20 | numpy.random.randint() -21 | numpy.random.random_integers() +22 | numpy.random.randn() +23 | numpy.random.randint() +24 | numpy.random.random_integers() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -22 | numpy.random.random_sample() -23 | numpy.random.choice() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | -NPY002.py:22:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` +NPY002.py:25:1: NPY002 Replace legacy `np.random.random_sample` call with `np.random.Generator` | -20 | numpy.random.randint() -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() +23 | numpy.random.randint() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() | ^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -23 | numpy.random.choice() -24 | numpy.random.bytes() +26 | numpy.random.choice() +27 | numpy.random.bytes() | -NPY002.py:23:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` +NPY002.py:26:1: NPY002 Replace legacy `np.random.choice` call with `np.random.Generator` | -21 | numpy.random.random_integers() -22 | numpy.random.random_sample() -23 | numpy.random.choice() +24 | numpy.random.random_integers() +25 | numpy.random.random_sample() +26 | numpy.random.choice() | ^^^^^^^^^^^^^^^^^^^ NPY002 -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | -NPY002.py:24:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` +NPY002.py:27:1: NPY002 Replace legacy `np.random.bytes` call with `np.random.Generator` | -22 | numpy.random.random_sample() -23 | numpy.random.choice() -24 | numpy.random.bytes() +25 | numpy.random.random_sample() +26 | numpy.random.choice() +27 | numpy.random.bytes() | ^^^^^^^^^^^^^^^^^^ NPY002 -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | -NPY002.py:25:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` +NPY002.py:28:1: NPY002 Replace legacy `np.random.shuffle` call with `np.random.Generator` | -23 | numpy.random.choice() -24 | numpy.random.bytes() -25 | numpy.random.shuffle() +26 | numpy.random.choice() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -26 | numpy.random.permutation() -27 | numpy.random.beta() +29 | numpy.random.permutation() +30 | numpy.random.beta() | -NPY002.py:26:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` +NPY002.py:29:1: NPY002 Replace legacy `np.random.permutation` call with `np.random.Generator` | -24 | numpy.random.bytes() -25 | numpy.random.shuffle() -26 | numpy.random.permutation() +27 | numpy.random.bytes() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -27 | numpy.random.beta() -28 | numpy.random.binomial() +30 | numpy.random.beta() +31 | numpy.random.binomial() | -NPY002.py:27:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` +NPY002.py:30:1: NPY002 Replace legacy `np.random.beta` call with `np.random.Generator` | -25 | numpy.random.shuffle() -26 | numpy.random.permutation() -27 | numpy.random.beta() +28 | numpy.random.shuffle() +29 | numpy.random.permutation() +30 | numpy.random.beta() | ^^^^^^^^^^^^^^^^^ NPY002 -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | -NPY002.py:28:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` +NPY002.py:31:1: NPY002 Replace legacy `np.random.binomial` call with `np.random.Generator` | -26 | numpy.random.permutation() -27 | numpy.random.beta() -28 | numpy.random.binomial() +29 | numpy.random.permutation() +30 | numpy.random.beta() +31 | numpy.random.binomial() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | -NPY002.py:29:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` +NPY002.py:32:1: NPY002 Replace legacy `np.random.chisquare` call with `np.random.Generator` | -27 | numpy.random.beta() -28 | numpy.random.binomial() -29 | numpy.random.chisquare() +30 | numpy.random.beta() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | -NPY002.py:30:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` +NPY002.py:33:1: NPY002 Replace legacy `np.random.dirichlet` call with `np.random.Generator` | -28 | numpy.random.binomial() -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() +31 | numpy.random.binomial() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -31 | numpy.random.exponential() -32 | numpy.random.f() +34 | numpy.random.exponential() +35 | numpy.random.f() | -NPY002.py:31:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` +NPY002.py:34:1: NPY002 Replace legacy `np.random.exponential` call with `np.random.Generator` | -29 | numpy.random.chisquare() -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() +32 | numpy.random.chisquare() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -32 | numpy.random.f() -33 | numpy.random.gamma() +35 | numpy.random.f() +36 | numpy.random.gamma() | -NPY002.py:32:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` +NPY002.py:35:1: NPY002 Replace legacy `np.random.f` call with `np.random.Generator` | -30 | numpy.random.dirichlet() -31 | numpy.random.exponential() -32 | numpy.random.f() +33 | numpy.random.dirichlet() +34 | numpy.random.exponential() +35 | numpy.random.f() | ^^^^^^^^^^^^^^ NPY002 -33 | numpy.random.gamma() -34 | numpy.random.geometric() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | -NPY002.py:33:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` +NPY002.py:36:1: NPY002 Replace legacy `np.random.gamma` call with `np.random.Generator` | -31 | numpy.random.exponential() -32 | numpy.random.f() -33 | numpy.random.gamma() +34 | numpy.random.exponential() +35 | numpy.random.f() +36 | numpy.random.gamma() | ^^^^^^^^^^^^^^^^^^ NPY002 -34 | numpy.random.geometric() -35 | numpy.random.get_state() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | -NPY002.py:34:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` +NPY002.py:37:1: NPY002 Replace legacy `np.random.geometric` call with `np.random.Generator` | -32 | numpy.random.f() -33 | numpy.random.gamma() -34 | numpy.random.geometric() +35 | numpy.random.f() +36 | numpy.random.gamma() +37 | numpy.random.geometric() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | -NPY002.py:35:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` +NPY002.py:38:1: NPY002 Replace legacy `np.random.get_state` call with `np.random.Generator` | -33 | numpy.random.gamma() -34 | numpy.random.geometric() -35 | numpy.random.get_state() +36 | numpy.random.gamma() +37 | numpy.random.geometric() +38 | numpy.random.get_state() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | -NPY002.py:36:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` +NPY002.py:39:1: NPY002 Replace legacy `np.random.gumbel` call with `np.random.Generator` | -34 | numpy.random.geometric() -35 | numpy.random.get_state() -36 | numpy.random.gumbel() +37 | numpy.random.geometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() | ^^^^^^^^^^^^^^^^^^^ NPY002 -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | -NPY002.py:37:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` +NPY002.py:40:1: NPY002 Replace legacy `np.random.hypergeometric` call with `np.random.Generator` | -35 | numpy.random.get_state() -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() +38 | numpy.random.get_state() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -38 | numpy.random.laplace() -39 | numpy.random.logistic() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | -NPY002.py:38:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` +NPY002.py:41:1: NPY002 Replace legacy `np.random.laplace` call with `np.random.Generator` | -36 | numpy.random.gumbel() -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() +39 | numpy.random.gumbel() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | -NPY002.py:39:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` +NPY002.py:42:1: NPY002 Replace legacy `np.random.logistic` call with `np.random.Generator` | -37 | numpy.random.hypergeometric() -38 | numpy.random.laplace() -39 | numpy.random.logistic() +40 | numpy.random.hypergeometric() +41 | numpy.random.laplace() +42 | numpy.random.logistic() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | -NPY002.py:40:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` +NPY002.py:43:1: NPY002 Replace legacy `np.random.lognormal` call with `np.random.Generator` | -38 | numpy.random.laplace() -39 | numpy.random.logistic() -40 | numpy.random.lognormal() +41 | numpy.random.laplace() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | -NPY002.py:41:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` +NPY002.py:44:1: NPY002 Replace legacy `np.random.logseries` call with `np.random.Generator` | -39 | numpy.random.logistic() -40 | numpy.random.lognormal() -41 | numpy.random.logseries() +42 | numpy.random.logistic() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() | ^^^^^^^^^^^^^^^^^^^^^^ NPY002 -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | -NPY002.py:42:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` +NPY002.py:45:1: NPY002 Replace legacy `np.random.multinomial` call with `np.random.Generator` | -40 | numpy.random.lognormal() -41 | numpy.random.logseries() -42 | numpy.random.multinomial() +43 | numpy.random.lognormal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() | ^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | -NPY002.py:43:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` +NPY002.py:46:1: NPY002 Replace legacy `np.random.multivariate_normal` call with `np.random.Generator` | -41 | numpy.random.logseries() -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() +44 | numpy.random.logseries() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | -NPY002.py:44:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` +NPY002.py:47:1: NPY002 Replace legacy `np.random.negative_binomial` call with `np.random.Generator` | -42 | numpy.random.multinomial() -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() +45 | numpy.random.multinomial() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | -NPY002.py:45:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` +NPY002.py:48:1: NPY002 Replace legacy `np.random.noncentral_chisquare` call with `np.random.Generator` | -43 | numpy.random.multivariate_normal() -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() +46 | numpy.random.multivariate_normal() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | -NPY002.py:46:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` +NPY002.py:49:1: NPY002 Replace legacy `np.random.noncentral_f` call with `np.random.Generator` | -44 | numpy.random.negative_binomial() -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() +47 | numpy.random.negative_binomial() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() | ^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -47 | numpy.random.normal() -48 | numpy.random.pareto() +50 | numpy.random.normal() +51 | numpy.random.pareto() | -NPY002.py:47:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` +NPY002.py:50:1: NPY002 Replace legacy `np.random.normal` call with `np.random.Generator` | -45 | numpy.random.noncentral_chisquare() -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() +48 | numpy.random.noncentral_chisquare() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() | ^^^^^^^^^^^^^^^^^^^ NPY002 -48 | numpy.random.pareto() -49 | numpy.random.poisson() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | -NPY002.py:48:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` +NPY002.py:51:1: NPY002 Replace legacy `np.random.pareto` call with `np.random.Generator` | -46 | numpy.random.noncentral_f() -47 | numpy.random.normal() -48 | numpy.random.pareto() +49 | numpy.random.noncentral_f() +50 | numpy.random.normal() +51 | numpy.random.pareto() | ^^^^^^^^^^^^^^^^^^^ NPY002 -49 | numpy.random.poisson() -50 | numpy.random.power() +52 | numpy.random.poisson() +53 | numpy.random.power() | -NPY002.py:49:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` +NPY002.py:52:1: NPY002 Replace legacy `np.random.poisson` call with `np.random.Generator` | -47 | numpy.random.normal() -48 | numpy.random.pareto() -49 | numpy.random.poisson() +50 | numpy.random.normal() +51 | numpy.random.pareto() +52 | numpy.random.poisson() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -50 | numpy.random.power() -51 | numpy.random.rayleigh() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | -NPY002.py:50:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` +NPY002.py:53:1: NPY002 Replace legacy `np.random.power` call with `np.random.Generator` | -48 | numpy.random.pareto() -49 | numpy.random.poisson() -50 | numpy.random.power() +51 | numpy.random.pareto() +52 | numpy.random.poisson() +53 | numpy.random.power() | ^^^^^^^^^^^^^^^^^^ NPY002 -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | -NPY002.py:51:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` +NPY002.py:54:1: NPY002 Replace legacy `np.random.rayleigh` call with `np.random.Generator` | -49 | numpy.random.poisson() -50 | numpy.random.power() -51 | numpy.random.rayleigh() +52 | numpy.random.poisson() +53 | numpy.random.power() +54 | numpy.random.rayleigh() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | -NPY002.py:52:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` +NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_cauchy` call with `np.random.Generator` | -50 | numpy.random.power() -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() +53 | numpy.random.power() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | -NPY002.py:53:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` +NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_exponential` call with `np.random.Generator` | -51 | numpy.random.rayleigh() -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() +54 | numpy.random.rayleigh() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | -NPY002.py:54:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` +NPY002.py:57:1: NPY002 Replace legacy `np.random.standard_gamma` call with `np.random.Generator` | -52 | numpy.random.standard_cauchy() -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() +55 | numpy.random.standard_cauchy() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | -NPY002.py:55:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` +NPY002.py:58:1: NPY002 Replace legacy `np.random.standard_normal` call with `np.random.Generator` | -53 | numpy.random.standard_exponential() -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() +56 | numpy.random.standard_exponential() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | -NPY002.py:56:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` +NPY002.py:59:1: NPY002 Replace legacy `np.random.standard_t` call with `np.random.Generator` | -54 | numpy.random.standard_gamma() -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() +57 | numpy.random.standard_gamma() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -57 | numpy.random.triangular() -58 | numpy.random.uniform() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | -NPY002.py:57:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` +NPY002.py:60:1: NPY002 Replace legacy `np.random.triangular` call with `np.random.Generator` | -55 | numpy.random.standard_normal() -56 | numpy.random.standard_t() -57 | numpy.random.triangular() +58 | numpy.random.standard_normal() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() | ^^^^^^^^^^^^^^^^^^^^^^^ NPY002 -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | -NPY002.py:58:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` +NPY002.py:61:1: NPY002 Replace legacy `np.random.uniform` call with `np.random.Generator` | -56 | numpy.random.standard_t() -57 | numpy.random.triangular() -58 | numpy.random.uniform() +59 | numpy.random.standard_t() +60 | numpy.random.triangular() +61 | numpy.random.uniform() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -59 | numpy.random.vonmises() -60 | numpy.random.wald() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | -NPY002.py:59:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` +NPY002.py:62:1: NPY002 Replace legacy `np.random.vonmises` call with `np.random.Generator` | -57 | numpy.random.triangular() -58 | numpy.random.uniform() -59 | numpy.random.vonmises() +60 | numpy.random.triangular() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() | ^^^^^^^^^^^^^^^^^^^^^ NPY002 -60 | numpy.random.wald() -61 | numpy.random.weibull() +63 | numpy.random.wald() +64 | numpy.random.weibull() | -NPY002.py:60:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` +NPY002.py:63:1: NPY002 Replace legacy `np.random.wald` call with `np.random.Generator` | -58 | numpy.random.uniform() -59 | numpy.random.vonmises() -60 | numpy.random.wald() +61 | numpy.random.uniform() +62 | numpy.random.vonmises() +63 | numpy.random.wald() | ^^^^^^^^^^^^^^^^^ NPY002 -61 | numpy.random.weibull() -62 | numpy.random.zipf() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | -NPY002.py:61:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` +NPY002.py:64:1: NPY002 Replace legacy `np.random.weibull` call with `np.random.Generator` | -59 | numpy.random.vonmises() -60 | numpy.random.wald() -61 | numpy.random.weibull() +62 | numpy.random.vonmises() +63 | numpy.random.wald() +64 | numpy.random.weibull() | ^^^^^^^^^^^^^^^^^^^^ NPY002 -62 | numpy.random.zipf() +65 | numpy.random.zipf() | -NPY002.py:62:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` +NPY002.py:65:1: NPY002 Replace legacy `np.random.zipf` call with `np.random.Generator` | -60 | numpy.random.wald() -61 | numpy.random.weibull() -62 | numpy.random.zipf() +63 | numpy.random.wald() +64 | numpy.random.weibull() +65 | numpy.random.zipf() | ^^^^^^^^^^^^^^^^^ NPY002 | From 6cc04d64e43f6353abcea1295c47cf4ee333e13e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 21:09:49 -0400 Subject: [PATCH 04/27] [`flake8-django`] Skip duplicate violations in `DJ012` (#5469) ## Summary This PR reduces the noise from `DJ012` by emitting a single violation when you have multiple consecutive violations of the same "type". For example, given: ```py class MultipleConsecutiveFields(models.Model): """Model that contains multiple out-of-order field definitions in a row.""" class Meta: verbose_name = "test" first_name = models.CharField(max_length=32) last_name = models.CharField(max_length=32) ``` It's convenient to only error on `first_name`, and not `last_name`, since we're really flagging that the _section_ is out-of-order. Closes #5465. --- .../test/fixtures/flake8_django/DJ012.py | 16 ++++++ .../rules/unordered_body_content_in_model.rs | 57 ++++++++++++------- ..._flake8_django__tests__DJ012_DJ012.py.snap | 17 ++++++ 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py index fc0ffd0cbb..a0f8d9da22 100644 --- a/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py +++ b/crates/ruff/resources/test/fixtures/flake8_django/DJ012.py @@ -111,3 +111,19 @@ class PerfectlyFine(models.Model): @property def random_property(self): return "%s" % self + + +class MultipleConsecutiveFields(models.Model): + """Model that contains multiple out-of-order field definitions in a row.""" + + + class Meta: + verbose_name = "test" + + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + + def get_absolute_url(self): + pass + + middle_name = models.CharField(max_length=32) diff --git a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs index 6404a105f6..d3734537a6 100644 --- a/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs +++ b/crates/ruff/src/rules/flake8_django/rules/unordered_body_content_in_model.rs @@ -63,20 +63,23 @@ use super::helpers; /// [Django Style Guide]: https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#model-style #[violation] pub struct DjangoUnorderedBodyContentInModel { - elem_type: ContentType, - before: ContentType, + element_type: ContentType, + prev_element_type: ContentType, } impl Violation for DjangoUnorderedBodyContentInModel { #[derive_message_formats] fn message(&self) -> String { - let DjangoUnorderedBodyContentInModel { elem_type, before } = self; - format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {elem_type} should come before {before}") + let DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + } = self; + format!("Order of model's inner classes, methods, and fields does not follow the Django Style Guide: {element_type} should come before {prev_element_type}") } } #[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] -pub(crate) enum ContentType { +enum ContentType { FieldDeclaration, ManagerDeclaration, MetaClass, @@ -149,24 +152,38 @@ pub(crate) fn unordered_body_content_in_model( { return; } - let mut elements_type_found = Vec::new(); + + // Track all the element types we've seen so far. + let mut element_types = Vec::new(); + let mut prev_element_type = None; for element in body.iter() { - let Some(current_element_type) = get_element_type(element, checker.semantic()) else { + let Some(element_type) = get_element_type(element, checker.semantic()) else { continue; }; - let Some(&element_type) = elements_type_found + + // Skip consecutive elements of the same type. It's less noisy to only report + // violations at type boundaries (e.g., avoid raising a violation for _every_ + // field declaration that's out of order). + if prev_element_type == Some(element_type) { + continue; + } + + prev_element_type = Some(element_type); + + if let Some(&prev_element_type) = element_types .iter() - .find(|&&element_type| element_type > current_element_type) else { - elements_type_found.push(current_element_type); - continue; - }; - let diagnostic = Diagnostic::new( - DjangoUnorderedBodyContentInModel { - elem_type: current_element_type, - before: element_type, - }, - element.range(), - ); - checker.diagnostics.push(diagnostic); + .find(|&&prev_element_type| prev_element_type > element_type) + { + let diagnostic = Diagnostic::new( + DjangoUnorderedBodyContentInModel { + element_type, + prev_element_type, + }, + element.range(), + ); + checker.diagnostics.push(diagnostic); + } else { + element_types.push(element_type); + } } } diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap index 835df4524c..d187a99a41 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ012_DJ012.py.snap @@ -37,4 +37,21 @@ DJ012.py:69:5: DJ012 Order of model's inner classes, methods, and fields does no | |____________^ DJ012 | +DJ012.py:123:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +121 | verbose_name = "test" +122 | +123 | first_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 +124 | last_name = models.CharField(max_length=32) + | + +DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class + | +127 | pass +128 | +129 | middle_name = models.CharField(max_length=32) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012 + | + From c8b9a46e2b5fd23774e72add4f2a64e2e38cf671 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Jul 2023 22:11:31 -0400 Subject: [PATCH 05/27] [`pyupgrade`] Restore the `keep-runtime-typing` setting (#5470) ## Summary This PR reverts #4427. See the included documentation for a detailed explanation. Closes #5434. --- BREAKING_CHANGES.md | 26 +++++ crates/ruff/src/checkers/ast/mod.rs | 12 +- ...__numpy-deprecated-function_NPY003.py.snap | 103 ++++++++++++++++++ crates/ruff/src/rules/pyupgrade/mod.rs | 34 ++++++ crates/ruff/src/rules/pyupgrade/settings.rs | 76 +++++++++++++ ..._annotations_keep_runtime_typing_p310.snap | 75 +++++++++++++ ...e_annotations_keep_runtime_typing_p37.snap | 4 + crates/ruff/src/settings/configuration.rs | 5 +- crates/ruff/src/settings/defaults.rs | 11 +- crates/ruff/src/settings/mod.rs | 7 +- crates/ruff/src/settings/options.rs | 5 +- crates/ruff_wasm/src/lib.rs | 3 +- ruff.schema.json | 25 +++++ 13 files changed, 373 insertions(+), 13 deletions(-) create mode 100644 crates/ruff/src/rules/pyupgrade/settings.rs create mode 100644 crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap create mode 100644 crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 7ae6cfdf97..6f1c0ae85f 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -1,5 +1,31 @@ # Breaking Changes +## 0.0.276 + +### The `keep-runtime-typing` setting has been reinstated ([#5470](https://github.com/astral-sh/ruff/pull/5470)) + +The `keep-runtime-typing` setting has been reinstated with revised semantics. This setting was +removed in [#4427](https://github.com/astral-sh/ruff/pull/4427), as it was equivalent to ignoring +the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism. + +Taking `UP006` (rewrite `List[int]` to `list[int]`) as an example, the setting now behaves as +follows: + +- On Python 3.7 and Python 3.8, setting `keep-runtime-typing = true` will cause Ruff to ignore + `UP006` violations, even if `from __future__ import annotations` is present in the file. + While such annotations are valid in Python 3.7 and Python 3.8 when combined with + `from __future__ import annotations`, they aren't supported by libraries like Pydantic and + FastAPI, which rely on runtime type checking. +- On Python 3.9 and above, the setting has no effect, as `list[int]` is a valid type annotation, + and libraries like Pydantic and FastAPI support it without issue. + +In short: `keep-runtime-typing` can be used to ensure that Ruff doesn't introduce type annotations +that are not supported at runtime by the current Python version, which are unsupported by libraries +like Pydantic and FastAPI. + +Note that this is not a breaking change, but is included here to complement the previous removal +of `keep-runtime-typing`. + ## 0.0.268 ### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427)) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 30c5c539b0..62f3736d8c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2108,6 +2108,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, value, @@ -2118,7 +2119,8 @@ where if self.settings.target_version >= PythonVersion::Py310 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep604_annotation( self, expr, slice, operator, @@ -2216,6 +2218,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2226,7 +2229,8 @@ where if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation( self, @@ -2291,6 +2295,7 @@ where && self.settings.target_version >= PythonVersion::Py37 && !self.semantic.future_annotations() && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing { flake8_future_annotations::rules::future_rewritable_type_annotation( self, expr, @@ -2301,7 +2306,8 @@ where if self.settings.target_version >= PythonVersion::Py39 || (self.settings.target_version >= PythonVersion::Py37 && self.semantic.future_annotations() - && self.semantic.in_annotation()) + && self.semantic.in_annotation() + && !self.settings.pyupgrade.keep_runtime_typing) { pyupgrade::rules::use_pep585_annotation(self, expr, &replacement); } diff --git a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap index 6821b3e2d2..1165e5f488 100644 --- a/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap +++ b/crates/ruff/src/rules/numpy/snapshots/ruff__rules__numpy__tests__numpy-deprecated-function_NPY003.py.snap @@ -60,6 +60,7 @@ NPY003.py:5:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instea 5 |+np.cumprod(np.random.rand(5, 5)) 6 6 | np.sometrue(np.random.rand(5, 5)) 7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead | @@ -78,6 +79,8 @@ NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead 6 |-np.sometrue(np.random.rand(5, 5)) 6 |+np.any(np.random.rand(5, 5)) 7 7 | np.alltrue(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead | @@ -85,6 +88,8 @@ NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead 6 | np.sometrue(np.random.rand(5, 5)) 7 | np.alltrue(np.random.rand(5, 5)) | ^^^^^^^^^^ NPY003 +8 | +9 | from numpy import round_, product, cumproduct, sometrue, alltrue | = help: Replace with `np.all` @@ -94,5 +99,103 @@ NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead 6 6 | np.sometrue(np.random.rand(5, 5)) 7 |-np.alltrue(np.random.rand(5, 5)) 7 |+np.all(np.random.rand(5, 5)) +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | + +NPY003.py:11:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead + | + 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 | +11 | round_(np.random.rand(5, 5), 2) + | ^^^^^^ NPY003 +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | + = help: Replace with `np.round` + +ℹ Suggested fix +8 8 | +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 |-round_(np.random.rand(5, 5), 2) + 11 |+round(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) + +NPY003.py:12:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | + = help: Replace with `np.prod` + +ℹ Suggested fix +9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 |-product(np.random.rand(5, 5)) + 12 |+prod(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:13:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead + | +11 | round_(np.random.rand(5, 5), 2) +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) + | ^^^^^^^^^^ NPY003 +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.cumprod` + +ℹ Suggested fix +10 10 | +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 |-cumproduct(np.random.rand(5, 5)) + 13 |+cumprod(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:14:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead + | +12 | product(np.random.rand(5, 5)) +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) + | ^^^^^^^^ NPY003 +15 | alltrue(np.random.rand(5, 5)) + | + = help: Replace with `np.any` + +ℹ Suggested fix +11 11 | round_(np.random.rand(5, 5), 2) +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 |-sometrue(np.random.rand(5, 5)) + 14 |+any(np.random.rand(5, 5)) +15 15 | alltrue(np.random.rand(5, 5)) + +NPY003.py:15:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead + | +13 | cumproduct(np.random.rand(5, 5)) +14 | sometrue(np.random.rand(5, 5)) +15 | alltrue(np.random.rand(5, 5)) + | ^^^^^^^ NPY003 + | + = help: Replace with `np.all` + +ℹ Suggested fix +12 12 | product(np.random.rand(5, 5)) +13 13 | cumproduct(np.random.rand(5, 5)) +14 14 | sometrue(np.random.rand(5, 5)) +15 |-alltrue(np.random.rand(5, 5)) + 15 |+all(np.random.rand(5, 5)) diff --git a/crates/ruff/src/rules/pyupgrade/mod.rs b/crates/ruff/src/rules/pyupgrade/mod.rs index 260ee6af1c..f65042ebc9 100644 --- a/crates/ruff/src/rules/pyupgrade/mod.rs +++ b/crates/ruff/src/rules/pyupgrade/mod.rs @@ -2,6 +2,7 @@ mod fixes; mod helpers; pub(crate) mod rules; +pub mod settings; pub(crate) mod types; #[cfg(test)] @@ -12,6 +13,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::rules::pyupgrade; use crate::settings::types::PythonVersion; use crate::test::test_path; use crate::{assert_messages, settings}; @@ -85,6 +87,38 @@ mod tests { Ok(()) } + #[test] + fn future_annotations_keep_runtime_typing_p37() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py37, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn future_annotations_keep_runtime_typing_p310() -> Result<()> { + let diagnostics = test_path( + Path::new("pyupgrade/future_annotations.py"), + &settings::Settings { + pyupgrade: pyupgrade::settings::Settings { + keep_runtime_typing: true, + }, + target_version: PythonVersion::Py310, + ..settings::Settings::for_rule(Rule::NonPEP585Annotation) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + #[test] fn future_annotations_pep_585_p37() -> Result<()> { let diagnostics = test_path( diff --git a/crates/ruff/src/rules/pyupgrade/settings.rs b/crates/ruff/src/rules/pyupgrade/settings.rs new file mode 100644 index 0000000000..a5b2d78188 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/settings.rs @@ -0,0 +1,76 @@ +//! Settings for the `pyupgrade` plugin. + +use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; +use serde::{Deserialize, Serialize}; + +#[derive( + Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, CombineOptions, +)] +#[serde( + deny_unknown_fields, + rename_all = "kebab-case", + rename = "PyUpgradeOptions" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct Options { + #[option( + default = r#"false"#, + value_type = "bool", + example = r#" + # Preserve types, even if a file imports `from __future__ import annotations`. + keep-runtime-typing = true + "# + )] + /// Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 + /// (`Union[str, int]` -> `str | int`) rewrites even if a file imports + /// `from __future__ import annotations`. + /// + /// This setting is only applicable when the target Python version is below + /// 3.9 and 3.10 respectively, and is most commonly used when working with + /// libraries like Pydantic and FastAPI, which rely on the ability to parse + /// type annotations at runtime. The use of `from __future__ import annotations` + /// causes Python to treat the type annotations as strings, which typically + /// allows for the use of language features that appear in later Python + /// versions but are not yet supported by the current version (e.g., `str | + /// int`). However, libraries that rely on runtime type annotations will + /// break if the annotations are incompatible with the current Python + /// version. + /// + /// For example, while the following is valid Python 3.8 code due to the + /// presence of `from __future__ import annotations`, the use of `str| int` + /// prior to Python 3.10 will cause Pydantic to raise a `TypeError` at + /// runtime: + /// + /// ```python + /// from __future__ import annotations + /// + /// import pydantic + /// + /// class Foo(pydantic.BaseModel): + /// bar: str | int + /// ``` + /// + /// + pub keep_runtime_typing: Option, +} + +#[derive(Debug, Default, CacheKey)] +pub struct Settings { + pub keep_runtime_typing: bool, +} + +impl From for Settings { + fn from(options: Options) -> Self { + Self { + keep_runtime_typing: options.keep_runtime_typing.unwrap_or_default(), + } + } +} + +impl From for Options { + fn from(settings: Settings) -> Self { + Self { + keep_runtime_typing: Some(settings.keep_runtime_typing), + } + } +} diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap new file mode 100644 index 0000000000..c9972fb6d9 --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p310.snap @@ -0,0 +1,75 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- +future_annotations.py:34:18: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: + | ^^^^ UP006 +35 | y = List[int]() +36 | y.append(x) + | + = help: Replace with `list` + +ℹ Fix +31 31 | return cls(x=0, y=0) +32 32 | +33 33 | +34 |-def f(x: int) -> List[int]: + 34 |+def f(x: int) -> list[int]: +35 35 | y = List[int]() +36 36 | y.append(x) +37 37 | return y + +future_annotations.py:35:9: UP006 [*] Use `list` instead of `List` for type annotation + | +34 | def f(x: int) -> List[int]: +35 | y = List[int]() + | ^^^^ UP006 +36 | y.append(x) +37 | return y + | + = help: Replace with `list` + +ℹ Fix +32 32 | +33 33 | +34 34 | def f(x: int) -> List[int]: +35 |- y = List[int]() + 35 |+ y = list[int]() +36 36 | y.append(x) +37 37 | return y +38 38 | + +future_annotations.py:42:27: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[list[int], List[str]] + +future_annotations.py:42:38: UP006 [*] Use `list` instead of `List` for type annotation + | +40 | x: Optional[int] = None +41 | +42 | MyList: TypeAlias = Union[List[int], List[str]] + | ^^^^ UP006 + | + = help: Replace with `list` + +ℹ Fix +39 39 | +40 40 | x: Optional[int] = None +41 41 | +42 |-MyList: TypeAlias = Union[List[int], List[str]] + 42 |+MyList: TypeAlias = Union[List[int], list[str]] + + diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap new file mode 100644 index 0000000000..870ad3bf5d --- /dev/null +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__future_annotations_keep_runtime_typing_p37.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyupgrade/mod.rs +--- + diff --git a/crates/ruff/src/settings/configuration.rs b/crates/ruff/src/settings/configuration.rs index 16143a83cb..653c328011 100644 --- a/crates/ruff/src/settings/configuration.rs +++ b/crates/ruff/src/settings/configuration.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::options::Options; use crate::settings::types::{ @@ -93,6 +93,7 @@ pub struct Configuration { pub pydocstyle: Option, pub pyflakes: Option, pub pylint: Option, + pub pyupgrade: Option, } impl Configuration { @@ -247,6 +248,7 @@ impl Configuration { pydocstyle: options.pydocstyle, pyflakes: options.pyflakes, pylint: options.pylint, + pyupgrade: options.pyupgrade, }) } @@ -334,6 +336,7 @@ impl Configuration { pydocstyle: self.pydocstyle.combine(config.pydocstyle), pyflakes: self.pyflakes.combine(config.pyflakes), pylint: self.pylint.combine(config.pylint), + pyupgrade: self.pyupgrade.combine(config.pyupgrade), } } } diff --git a/crates/ruff/src/settings/defaults.rs b/crates/ruff/src/settings/defaults.rs index 8ae6d16e73..987148705d 100644 --- a/crates/ruff/src/settings/defaults.rs +++ b/crates/ruff/src/settings/defaults.rs @@ -1,10 +1,11 @@ -use std::collections::HashSet; - use once_cell::sync::Lazy; use path_absolutize::path_dedot; use regex::Regex; use rustc_hash::FxHashSet; +use std::collections::HashSet; +use super::types::{FilePattern, PythonVersion}; +use super::Settings; use crate::codes::{self, RuleCodePrefix}; use crate::line_width::{LineLength, TabSize}; use crate::registry::Linter; @@ -14,13 +15,10 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::FilePatternSet; -use super::types::{FilePattern, PythonVersion}; -use super::Settings; - pub const PREFIXES: &[RuleSelector] = &[ prefix_to_selector(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E)), RuleSelector::Linter(Linter::Pyflakes), @@ -114,6 +112,7 @@ impl Default for Settings { pydocstyle: pydocstyle::settings::Settings::default(), pyflakes: pyflakes::settings::Settings::default(), pylint: pylint::settings::Settings::default(), + pyupgrade: pyupgrade::settings::Settings::default(), } } } diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 0f7b961734..f00a4833b2 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -20,7 +20,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::configuration::Configuration; use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat}; @@ -130,6 +130,7 @@ pub struct Settings { pub pydocstyle: pydocstyle::settings::Settings, pub pyflakes: pyflakes::settings::Settings, pub pylint: pylint::settings::Settings, + pub pyupgrade: pyupgrade::settings::Settings, } impl Settings { @@ -284,6 +285,10 @@ impl Settings { .pylint .map(pylint::settings::Settings::from) .unwrap_or_default(), + pyupgrade: config + .pyupgrade + .map(pyupgrade::settings::Settings::from) + .unwrap_or_default(), }) } diff --git a/crates/ruff/src/settings/options.rs b/crates/ruff/src/settings/options.rs index ef5a6a3597..f003b6bb5a 100644 --- a/crates/ruff/src/settings/options.rs +++ b/crates/ruff/src/settings/options.rs @@ -12,7 +12,7 @@ use crate::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use crate::settings::types::{PythonVersion, SerializationFormat, Version}; @@ -551,6 +551,9 @@ pub struct Options { #[option_group] /// Options for the `pylint` plugin. pub pylint: Option, + #[option_group] + /// Options for the `pyupgrade` plugin. + pub pyupgrade: Option, // Tables are required to go last. #[option( default = "{}", diff --git a/crates/ruff_wasm/src/lib.rs b/crates/ruff_wasm/src/lib.rs index 1fe7aa89d6..27e3de73c0 100644 --- a/crates/ruff_wasm/src/lib.rs +++ b/crates/ruff_wasm/src/lib.rs @@ -13,7 +13,7 @@ use ruff::rules::{ flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming, - pycodestyle, pydocstyle, pyflakes, pylint, + pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade, }; use ruff::settings::configuration::Configuration; use ruff::settings::options::Options; @@ -166,6 +166,7 @@ pub fn defaultSettings() -> Result { pydocstyle: Some(pydocstyle::settings::Settings::default().into()), pyflakes: Some(pyflakes::settings::Settings::default().into()), pylint: Some(pylint::settings::Settings::default().into()), + pyupgrade: Some(pyupgrade::settings::Settings::default().into()), })?) } diff --git a/ruff.schema.json b/ruff.schema.json index c6d3d474b7..8875d1423c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -475,6 +475,17 @@ } ] }, + "pyupgrade": { + "description": "Options for the `pyupgrade` plugin.", + "anyOf": [ + { + "$ref": "#/definitions/PyUpgradeOptions" + }, + { + "type": "null" + } + ] + }, "required-version": { "description": "Require a specific version of Ruff to be running (useful for unifying results across many environments, e.g., with a `pyproject.toml` file).", "anyOf": [ @@ -1419,6 +1430,19 @@ }, "additionalProperties": false }, + "PyUpgradeOptions": { + "type": "object", + "properties": { + "keep-runtime-typing": { + "description": "Whether to avoid PEP 585 (`List[int]` -> `list[int]`) and PEP 604 (`Union[str, int]` -> `str | int`) rewrites even if a file imports `from __future__ import annotations`.\n\nThis setting is only applicable when the target Python version is below 3.9 and 3.10 respectively, and is most commonly used when working with libraries like Pydantic and FastAPI, which rely on the ability to parse type annotations at runtime. The use of `from __future__ import annotations` causes Python to treat the type annotations as strings, which typically allows for the use of language features that appear in later Python versions but are not yet supported by the current version (e.g., `str | int`). However, libraries that rely on runtime type annotations will break if the annotations are incompatible with the current Python version.\n\nFor example, while the following is valid Python 3.8 code due to the presence of `from __future__ import annotations`, the use of `str| int` prior to Python 3.10 will cause Pydantic to raise a `TypeError` at runtime:\n\n```python from __future__ import annotations\n\nimport pydantic\n\nclass Foo(pydantic.BaseModel): bar: str | int ```", + "type": [ + "boolean", + "null" + ] + } + }, + "additionalProperties": false + }, "Pycodestyle": { "type": "object", "properties": { @@ -2042,6 +2066,7 @@ "NPY00", "NPY001", "NPY002", + "NPY003", "PD", "PD0", "PD00", From df13e69c3ce59bd052af969630758cac5c38f0a2 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Sun, 2 Jul 2023 19:13:35 -0700 Subject: [PATCH 06/27] Format let-else with rustfmt nightly (#5461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for `let…else` formatting was just merged to nightly (rust-lang/rust#113225). Rerun `cargo fmt` with Rust nightly 2023-07-02 to pick this up. Followup to #939. Signed-off-by: Anders Kaseorg --- crates/ruff/src/checkers/ast/mod.rs | 2 +- crates/ruff/src/jupyter/notebook.rs | 11 +- .../flake8_annotations/rules/definition.rs | 6 +- .../rules/hardcoded_sql_expression.rs | 2 +- .../rules/abstract_base_class.rs | 26 ++-- .../rules/assert_raises_exception.rs | 23 ++-- .../rules/assignment_to_os_environ.rs | 2 +- .../rules/duplicate_exceptions.rs | 6 +- .../except_with_non_exception_classes.rs | 2 +- .../rules/getattr_with_constant.rs | 5 +- .../rules/mutable_argument_default.rs | 2 +- .../redundant_tuple_in_exception_handler.rs | 6 +- .../rules/setattr_with_constant.rs | 3 +- .../star_arg_unpacking_after_keyword_arg.rs | 2 +- .../rules/strip_with_multi_characters.rs | 3 +- .../rules/unary_prefix_increment.rs | 6 +- .../rules/unreliable_callable_check.rs | 4 +- .../rules/zip_without_explicit_strict.rs | 8 +- .../src/rules/flake8_comprehensions/fixes.rs | 22 ++-- .../unnecessary_comprehension_any_all.rs | 6 +- .../unnecessary_double_cast_or_process.rs | 2 +- .../rules/unnecessary_generator_dict.rs | 4 +- .../rules/unnecessary_generator_list.rs | 4 +- .../rules/unnecessary_generator_set.rs | 4 +- .../unnecessary_list_comprehension_dict.rs | 4 +- .../unnecessary_list_comprehension_set.rs | 4 +- .../rules/unnecessary_literal_dict.rs | 4 +- .../rules/unnecessary_literal_set.rs | 4 +- .../rules/unnecessary_map.rs | 14 +- .../rules/unnecessary_subscript_reversal.rs | 18 ++- .../call_datetime_strptime_without_zone.rs | 12 +- .../rules/locals_in_render_function.rs | 2 +- .../rules/model_without_dunder_str.rs | 8 +- .../rules/nullable_model_string_field.rs | 8 +- .../rules/multiple_starts_ends_with.rs | 35 +++-- .../rules/iter_method_return_iterable.rs | 9 +- .../flake8_pyi/rules/prefix_type_params.rs | 35 +++-- .../rules/flake8_pyi/rules/simple_defaults.rs | 2 +- .../rules/str_or_repr_defined_in_stub.rs | 5 +- .../flake8_pytest_style/rules/assertion.rs | 3 +- .../src/rules/flake8_return/rules/function.rs | 10 +- .../flake8_simplify/rules/ast_bool_op.rs | 51 +++++++- .../rules/flake8_simplify/rules/ast_expr.rs | 30 ++++- .../src/rules/flake8_simplify/rules/ast_if.rs | 122 +++++++++++++++--- .../rules/flake8_simplify/rules/ast_ifexp.rs | 15 ++- .../flake8_simplify/rules/ast_unary_op.rs | 31 ++++- .../src/rules/flake8_simplify/rules/fix_if.rs | 3 +- .../rules/flake8_simplify/rules/fix_with.rs | 7 +- .../flake8_simplify/rules/key_in_dict.rs | 5 +- .../rules/open_file_with_context_handler.rs | 12 +- .../rules/reimplemented_builtin.rs | 54 ++++++-- .../flynt/rules/static_join_to_fstring.rs | 10 +- .../rules/isort/rules/add_required_imports.rs | 10 +- .../ruff/src/rules/pandas_vet/rules/call.rs | 2 +- crates/ruff/src/rules/pep8_naming/helpers.rs | 6 +- .../perflint/rules/incorrect_dict_iterator.rs | 10 +- .../perflint/rules/unnecessary_list_cast.rs | 10 +- .../rules/blank_before_after_class.rs | 5 +- .../rules/blank_before_after_function.rs | 3 +- .../src/rules/pydocstyle/rules/capitalized.rs | 2 +- .../src/rules/pydocstyle/rules/if_needed.rs | 3 +- .../rules/multi_line_summary_start.rs | 5 +- .../rules/pydocstyle/rules/no_signature.rs | 3 +- .../src/rules/pydocstyle/rules/sections.rs | 18 +-- .../pydocstyle/rules/starts_with_this.rs | 2 +- crates/ruff/src/rules/pyflakes/cformat.rs | 4 +- crates/ruff/src/rules/pyflakes/format.rs | 3 +- crates/ruff/src/rules/pylint/helpers.rs | 21 ++- .../src/rules/pylint/rules/import_self.rs | 3 +- .../pylint/rules/invalid_envvar_default.rs | 7 +- .../src/rules/pylint/rules/nested_min_max.rs | 2 +- .../unexpected_special_method_signature.rs | 4 +- .../src/rules/pylint/rules/useless_return.rs | 2 +- ...convert_named_tuple_functional_to_class.rs | 16 ++- .../convert_typed_dict_functional_to_class.rs | 11 +- .../pyupgrade/rules/deprecated_import.rs | 2 +- .../pyupgrade/rules/extraneous_parentheses.rs | 4 +- .../src/rules/pyupgrade/rules/f_strings.rs | 4 +- .../rules/lru_cache_with_maxsize_none.rs | 3 +- .../rules/lru_cache_without_parameters.rs | 3 +- .../rules/pyupgrade/rules/native_literals.rs | 15 ++- .../pyupgrade/rules/outdated_version_block.rs | 9 +- .../rules/super_call_with_parameters.rs | 16 ++- .../pyupgrade/rules/type_of_primitive.rs | 2 +- .../rules/unnecessary_encode_utf8.rs | 3 +- .../rules/unpacked_list_comprehension.rs | 3 +- .../pyupgrade/rules/useless_metaclass_type.rs | 4 +- .../rules/collection_literal_concatenation.rs | 10 +- .../explicit_f_string_type_conversion.rs | 3 +- .../src/rules/ruff/rules/implicit_optional.rs | 45 +++++-- .../rules/ruff/rules/pairwise_over_zipped.rs | 2 +- .../tryceratops/rules/useless_try_except.rs | 5 +- crates/ruff_cli/src/commands/show_settings.rs | 4 +- crates/ruff_dev/src/generate_options.rs | 12 +- .../ruff_macros/src/derive_message_formats.rs | 8 +- crates/ruff_macros/src/map_codes.rs | 26 +++- crates/ruff_macros/src/rule_namespace.rs | 37 ++++-- crates/ruff_python_ast/src/helpers.rs | 37 +++++- crates/ruff_python_ast/src/identifier.rs | 5 +- .../src/comments/placement.rs | 36 +++--- .../src/expression/expr_bool_op.rs | 2 +- .../src/statement/suite.rs | 2 +- crates/ruff_python_formatter/src/trivia.rs | 4 +- .../src/implicit_imports.rs | 2 +- .../src/analyze/logging.rs | 6 +- 105 files changed, 782 insertions(+), 362 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 62f3736d8c..97a2b0451f 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -4435,7 +4435,7 @@ impl<'a> Checker<'a> { } fn handle_node_delete(&mut self, expr: &'a Expr) { - let Expr::Name(ast::ExprName { id, .. } )= expr else { + let Expr::Name(ast::ExprName { id, .. }) = expr else { return; }; diff --git a/crates/ruff/src/jupyter/notebook.rs b/crates/ruff/src/jupyter/notebook.rs index 91aa62e24d..3c3c0154b3 100644 --- a/crates/ruff/src/jupyter/notebook.rs +++ b/crates/ruff/src/jupyter/notebook.rs @@ -268,11 +268,12 @@ impl Notebook { .markers() .iter() .rev() - .find(|m| m.source <= *offset) else { - // There are no markers above the current offset, so we can - // stop here. - break; - }; + .find(|m| m.source <= *offset) + else { + // There are no markers above the current offset, so we can + // stop here. + break; + }; last_marker = Some(marker); marker } diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 5fdb814dab..47232b9e04 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -457,11 +457,7 @@ pub(crate) fn definition( // TODO(charlie): Consider using the AST directly here rather than `Definition`. // We could adhere more closely to `flake8-annotations` by defining public // vs. secret vs. protected. - let Definition::Member(Member { - kind, - stmt, - .. - }) = definition else { + let Definition::Member(Member { kind, stmt, .. }) = definition else { return vec![]; }; diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 41e426b34d..51b200b67e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -67,7 +67,7 @@ fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Optio return None; }; // Only evaluate the full BinOp, not the nested components. - let Expr::BinOp(_ )= parent else { + let Expr::BinOp(_) = parent else { if any_over_expr(expr, &has_string_literal) { return Some(checker.generator().expr(expr)); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs index 796bcd17df..4c16954baa 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs @@ -161,19 +161,19 @@ pub(crate) fn abstract_base_class( continue; } - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, - body, - name: method_name, - .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + decorator_list, + body, + name: method_name, + .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + decorator_list, + body, + name: method_name, + .. + })) = stmt + else { continue; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs index d7a3994e76..00c44bf0ee 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assert_raises_exception.rs @@ -78,7 +78,13 @@ impl fmt::Display for ExceptionKind { /// B017 pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) { for item in items { - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &item.context_expr else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &item.context_expr + else { return; }; if args.len() != 1 { @@ -91,13 +97,14 @@ pub(crate) fn assert_raises_exception(checker: &mut Checker, items: &[WithItem]) let Some(exception) = checker .semantic() .resolve_call_path(args.first().unwrap()) - .and_then(|call_path| { - match call_path.as_slice() { - ["", "Exception"] => Some(ExceptionKind::Exception), - ["", "BaseException"] => Some(ExceptionKind::BaseException), - _ => None, - } - }) else { return; }; + .and_then(|call_path| match call_path.as_slice() { + ["", "Exception"] => Some(ExceptionKind::Exception), + ["", "BaseException"] => Some(ExceptionKind::BaseException), + _ => None, + }) + else { + return; + }; let assertion = if matches!(func.as_ref(), Expr::Attribute(ast::ExprAttribute { attr, .. }) if attr == "assertRaises") { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs index dbd9ad3d0e..50f3f32922 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/assignment_to_os_environ.rs @@ -59,7 +59,7 @@ pub(crate) fn assignment_to_os_environ(checker: &mut Checker, targets: &[Expr]) if attr != "environ" { return; } - let Expr::Name(ast::ExprName { id, .. } )= value.as_ref() else { + let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else { return; }; if id != "os" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs index 1e8f8edb18..388d4a0128 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_exceptions.rs @@ -166,7 +166,11 @@ pub(crate) fn duplicate_exceptions(checker: &mut Checker, handlers: &[ExceptHand let mut seen: FxHashSet = FxHashSet::default(); let mut duplicates: FxHashMap> = FxHashMap::default(); for handler in handlers { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; match type_.as_ref() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs index ab362356af..cd195a8635 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/except_with_non_exception_classes.rs @@ -47,7 +47,7 @@ impl Violation for ExceptWithNonExceptionClasses { /// This should leave any unstarred iterables alone (subsequently raising a /// warning for B029). fn flatten_starred_iterables(expr: &Expr) -> Vec<&Expr> { - let Expr::Tuple(ast::ExprTuple { elts, .. } )= expr else { + let Expr::Tuple(ast::ExprTuple { elts, .. }) = expr else { return vec![expr]; }; let mut flattened_exprs: Vec<&Expr> = Vec::with_capacity(elts.len()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs index 1fa1cd2634..3abcea7013 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/getattr_with_constant.rs @@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant( func: &Expr, args: &[Expr], ) { - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if id != "getattr" { @@ -76,7 +76,8 @@ pub(crate) fn getattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= arg else { + }) = arg + else { return; }; if !is_identifier(value) { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs index dc2a5ffbfe..4af5133187 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mutable_argument_default.rs @@ -69,7 +69,7 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, arguments: &Argume .chain(&arguments.args) .chain(&arguments.kwonlyargs) { - let Some(default)= default else { + let Some(default) = default else { continue; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs index 225807a217..865456d3f2 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/redundant_tuple_in_exception_handler.rs @@ -59,7 +59,11 @@ pub(crate) fn redundant_tuple_in_exception_handler( handlers: &[ExceptHandler], ) { for handler in handlers { - let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { type_: Some(type_), .. }) = handler else { + let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { + type_: Some(type_), + .. + }) = handler + else { continue; }; let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs index 7d1ddcaf4c..fe107f5fc0 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/setattr_with_constant.rs @@ -82,7 +82,8 @@ pub(crate) fn setattr_with_constant( let Expr::Constant(ast::ExprConstant { value: Constant::Str(name), .. - } )= name else { + }) = name + else { return; }; if !is_identifier(name) { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs index f8cb8fa12c..dae1e2168e 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/star_arg_unpacking_after_keyword_arg.rs @@ -64,7 +64,7 @@ pub(crate) fn star_arg_unpacking_after_keyword_arg( return; }; for arg in args { - let Expr::Starred (_) = arg else { + let Expr::Starred(_) = arg else { continue; }; if arg.start() <= keyword.start() { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs index 75349ba3d9..2098a2a796 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/strip_with_multi_characters.rs @@ -62,7 +62,8 @@ pub(crate) fn strip_with_multi_characters( let Expr::Constant(ast::ExprConstant { value: Constant::Str(value), .. - } )= &args[0] else { + }) = &args[0] + else { return; }; diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs index 43356e2fa4..e9a096aacc 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unary_prefix_increment.rs @@ -45,9 +45,9 @@ pub(crate) fn unary_prefix_increment( if !matches!(op, UnaryOp::UAdd) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op, .. })= operand else { - return; - }; + let Expr::UnaryOp(ast::ExprUnaryOp { op, .. }) = operand else { + return; + }; if !matches!(op, UnaryOp::UAdd) { return; } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs index 58cfafa186..34e209ee48 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/unreliable_callable_check.rs @@ -63,8 +63,8 @@ pub(crate) fn unreliable_callable_check( let Expr::Constant(ast::ExprConstant { value: Constant::Str(s), .. - }) = &args[1] else - { + }) = &args[1] + else { return; }; if s != "__call__" { diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs index 83854bc6bc..4a4efd4e3c 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/zip_without_explicit_strict.rs @@ -68,7 +68,13 @@ pub(crate) fn zip_without_explicit_strict( /// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to /// `itertools.cycle` or similar). fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool { - let Expr::Call(ast::ExprCall { func, args, keywords, .. }) = &arg else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + .. + }) = &arg + else { return false; }; diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index b32379677b..1d4db80bfd 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -109,7 +109,8 @@ pub(crate) fn fix_unnecessary_generator_dict( // Extract the (k, v) from `(k, v) for ...`. let generator_exp = match_generator_exp(&arg.value)?; let tuple = match_tuple(&generator_exp.elt)?; - let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] else { + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { bail!("Expected tuple to contain two elements"); }; @@ -188,9 +189,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( let tuple = match_tuple(&list_comp.elt)?; - let [Element::Simple { - value: key, .. - }, Element::Simple { value, .. }] = &tuple.elements[..] else { bail!("Expected tuple with two elements"); }; + let [Element::Simple { value: key, .. }, Element::Simple { value, .. }] = &tuple.elements[..] + else { + bail!("Expected tuple with two elements"); + }; tree = Expression::DictComp(Box::new(DictComp { key: Box::new(key.clone()), @@ -982,14 +984,10 @@ pub(crate) fn fix_unnecessary_map( } let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else { - bail!( - "Expected tuple to contain a key as the first element" - ); + bail!("Expected tuple to contain a key as the first element"); }; let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else { - bail!( - "Expected tuple to contain a key as the second element" - ); + bail!("Expected tuple to contain a key as the second element"); }; (key, value) @@ -1063,9 +1061,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all( let call = match_call_mut(&mut tree)?; let Expression::ListComp(list_comp) = &call.args[0].value else { - bail!( - "Expected Expression::ListComp" - ); + bail!("Expected Expression::ListComp"); }; let mut new_empty_lines = vec![]; diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs index ad910495a7..71b6432f2a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_comprehension_any_all.rs @@ -66,11 +66,13 @@ pub(crate) fn unnecessary_comprehension_any_all( if !keywords.is_empty() { return; } - let Expr::Name(ast::ExprName { id, .. } )= func else { + let Expr::Name(ast::ExprName { id, .. }) = func else { return; }; if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 { - let (Expr::ListComp(ast::ExprListComp { elt, .. } )| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] else { + let (Expr::ListComp(ast::ExprListComp { elt, .. }) + | Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0] + else { return; }; if contains_await(elt) { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs index 1ea1ae62fc..2c71d2d11a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_double_cast_or_process.rs @@ -84,7 +84,7 @@ pub(crate) fn unnecessary_double_cast_or_process( let Some(arg) = args.first() else { return; }; - let Expr::Call(ast::ExprCall { func, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, .. }) = arg else { return; }; let Some(inner) = helpers::expr_name(func) else { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs index 030eff86d3..fe8c8afb0e 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_dict.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs index d5f9cea172..470595f806 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_list.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_list( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("list", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("list") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs index 65af9cb79d..44fa61ced3 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_generator_set.rs @@ -49,7 +49,9 @@ pub(crate) fn unnecessary_generator_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs index e8cc9b954f..d425003f04 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_dict.rs @@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("dict") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs index 1dc3618654..9bf415f8f8 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_list_comprehension_set.rs @@ -47,7 +47,9 @@ pub(crate) fn unnecessary_list_comprehension_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs index 41e4866233..8c981d062a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_dict.rs @@ -54,7 +54,9 @@ pub(crate) fn unnecessary_literal_dict( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("dict", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("dict") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs index 6f3cf56fb3..43f4372404 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_literal_set.rs @@ -55,7 +55,9 @@ pub(crate) fn unnecessary_literal_set( args: &[Expr], keywords: &[Keyword], ) { - let Some(argument) = helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) else { + let Some(argument) = + helpers::exactly_one_argument_with_matching_function("set", func, args, keywords) + else { return; }; if !checker.semantic().is_builtin("set") { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs index 8a54343bbf..9de5aeec53 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_map.rs @@ -83,7 +83,7 @@ pub(crate) fn unnecessary_map( ) } - let Some(id) = helpers::expr_name(func) else { + let Some(id) = helpers::expr_name(func) else { return; }; match id { @@ -127,9 +127,11 @@ pub(crate) fn unnecessary_map( if args.len() != 2 { return; } - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { - return; - }; + let Some(argument) = + helpers::first_argument_with_matching_function("map", func, args) + else { + return; + }; if let Expr::Lambda(_) = argument { let mut diagnostic = create_diagnostic(id, expr.range()); if checker.patch(diagnostic.kind.rule()) { @@ -155,7 +157,9 @@ pub(crate) fn unnecessary_map( if args.len() == 1 { if let Expr::Call(ast::ExprCall { func, args, .. }) = &args[0] { - let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else { + let Some(argument) = + helpers::first_argument_with_matching_function("map", func, args) + else { return; }; if let Expr::Lambda(ast::ExprLambda { body, .. }) = argument { diff --git a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs index 5a2c0879cc..896b9128a5 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/rules/unnecessary_subscript_reversal.rs @@ -64,9 +64,15 @@ pub(crate) fn unnecessary_subscript_reversal( let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else { return; }; - let Expr::Slice(ast::ExprSlice { lower, upper, step, range: _ }) = slice.as_ref() else { - return; - }; + let Expr::Slice(ast::ExprSlice { + lower, + upper, + step, + range: _, + }) = slice.as_ref() + else { + return; + }; if lower.is_some() || upper.is_some() { return; } @@ -77,13 +83,15 @@ pub(crate) fn unnecessary_subscript_reversal( op: UnaryOp::USub, operand, range: _, - }) = step.as_ref() else { + }) = step.as_ref() + else { return; }; let Expr::Constant(ast::ExprConstant { value: Constant::Int(val), .. - }) = operand.as_ref() else { + }) = operand.as_ref() + else { return; }; if *val != BigInt::from(1) { diff --git a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs index 200a75d0bf..f690027e80 100644 --- a/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs +++ b/crates/ruff/src/rules/flake8_datetimez/rules/call_datetime_strptime_without_zone.rs @@ -49,11 +49,13 @@ pub(crate) fn call_datetime_strptime_without_zone( } }; - let (Some(grandparent), Some(parent)) = (checker.semantic().expr_grandparent(), checker.semantic().expr_parent()) else { - checker.diagnostics.push(Diagnostic::new( - CallDatetimeStrptimeWithoutZone, - location, - )); + let (Some(grandparent), Some(parent)) = ( + checker.semantic().expr_grandparent(), + checker.semantic().expr_parent(), + ) else { + checker + .diagnostics + .push(Diagnostic::new(CallDatetimeStrptimeWithoutZone, location)); return; }; diff --git a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs index b5d3340a11..50cd33d67d 100644 --- a/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs +++ b/crates/ruff/src/rules/flake8_django/rules/locals_in_render_function.rs @@ -85,7 +85,7 @@ pub(crate) fn locals_in_render_function( fn is_locals_call(expr: &Expr, semantic: &SemanticModel) -> bool { let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false + return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { matches!(call_path.as_slice(), ["", "locals"]) diff --git a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs index a02992757e..31d0ea65a9 100644 --- a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -96,18 +96,18 @@ fn is_non_abstract_model(bases: &[Expr], body: &[Stmt], semantic: &SemanticModel /// Check if class is abstract, in terms of Django model inheritance. fn is_model_abstract(body: &[Stmt]) -> bool { for element in body.iter() { - let Stmt::ClassDef(ast::StmtClassDef {name, body, ..}) = element else { - continue + let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else { + continue; }; if name != "Meta" { continue; } for element in body.iter() { - let Stmt::Assign(ast::StmtAssign {targets, value, ..}) = element else { + let Stmt::Assign(ast::StmtAssign { targets, value, .. }) = element else { continue; }; for target in targets.iter() { - let Expr::Name(ast::ExprName {id , ..}) = target else { + let Expr::Name(ast::ExprName { id, .. }) = target else { continue; }; if id != "abstract" { diff --git a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs index a6ed90c648..6da8d4ba50 100644 --- a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -64,8 +64,8 @@ const NOT_NULL_TRUE_FIELDS: [&str; 6] = [ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec { let mut errors = Vec::new(); for statement in body.iter() { - let Stmt::Assign(ast::StmtAssign {value, ..}) = statement else { - continue + let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { + continue; }; if let Some(field_name) = is_nullable_field(checker, value) { errors.push(Diagnostic::new( @@ -80,7 +80,7 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> V } fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> { - let Expr::Call(ast::ExprCall {func, keywords, ..}) = value else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = value else { return None; }; @@ -97,7 +97,7 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st let mut unique_key = false; for keyword in keywords.iter() { let Some(argument) = &keyword.arg else { - continue + continue; }; if !is_const_true(&keyword.value) { continue; diff --git a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs index 2905bf8d34..05893da8e5 100644 --- a/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs +++ b/crates/ruff/src/rules/flake8_pie/rules/multiple_starts_ends_with.rs @@ -60,7 +60,12 @@ impl AlwaysAutofixableViolation for MultipleStartsEndsWith { /// PIE810 pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _ }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -70,24 +75,25 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { func, args, keywords, - range: _ - }) = &call else { - continue + range: _, + }) = &call + else { + continue; }; if !(args.len() == 1 && keywords.is_empty()) { continue; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { - continue + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { + continue; }; if attr != "startswith" && attr != "endswith" { continue; } - let Expr::Name(ast::ExprName { id: arg_name, .. } )= value.as_ref() else { - continue + let Expr::Name(ast::ExprName { id: arg_name, .. }) = value.as_ref() else { + continue; }; duplicates @@ -110,8 +116,17 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) { .iter() .map(|index| &values[*index]) .map(|expr| { - let Expr::Call(ast::ExprCall { func: _, args, keywords: _, range: _}) = expr else { - unreachable!("{}", format!("Indices should only contain `{attr_name}` calls")) + let Expr::Call(ast::ExprCall { + func: _, + args, + keywords: _, + range: _, + }) = expr + else { + unreachable!( + "{}", + format!("Indices should only contain `{attr_name}` calls") + ) }; args.get(0) .unwrap_or_else(|| panic!("`{attr_name}` should have one argument")) diff --git a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs index 09347b72d2..e4971016b1 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/iter_method_return_iterable.rs @@ -70,15 +70,12 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De kind: MemberKind::Method, stmt, .. - }) = definition else { + }) = definition + else { return; }; - let Stmt::FunctionDef(ast::StmtFunctionDef { - name, - returns, - .. - }) = stmt else { + let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else { return; }; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs index 07f62306b7..5a17dddcf7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/prefix_type_params.rs @@ -70,17 +70,30 @@ pub(crate) fn prefix_type_params(checker: &mut Checker, value: &Expr, targets: & }; if let Expr::Call(ast::ExprCall { func, .. }) = value { - let Some(kind) = checker.semantic().resolve_call_path(func).and_then(|call_path| { - if checker.semantic().match_typing_call_path(&call_path, "ParamSpec") { - Some(VarKind::ParamSpec) - } else if checker.semantic().match_typing_call_path(&call_path, "TypeVar") { - Some(VarKind::TypeVar) - } else if checker.semantic().match_typing_call_path(&call_path, "TypeVarTuple") { - Some(VarKind::TypeVarTuple) - } else { - None - } - }) else { + let Some(kind) = checker + .semantic() + .resolve_call_path(func) + .and_then(|call_path| { + if checker + .semantic() + .match_typing_call_path(&call_path, "ParamSpec") + { + Some(VarKind::ParamSpec) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVar") + { + Some(VarKind::TypeVar) + } else if checker + .semantic() + .match_typing_call_path(&call_path, "TypeVarTuple") + { + Some(VarKind::TypeVarTuple) + } else { + None + } + }) + else { return; }; checker diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index eacd3a98dd..ec891299d9 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -270,7 +270,7 @@ fn is_valid_default_value_without_annotation(default: &Expr) -> bool { /// Returns `true` if an [`Expr`] appears to be `TypeVar`, `TypeVarTuple`, `NewType`, or `ParamSpec` /// call. fn is_type_var_like_call(expr: &Expr, semantic: &SemanticModel) -> bool { - let Expr::Call(ast::ExprCall { func, .. } )= expr else { + let Expr::Call(ast::ExprCall { func, .. }) = expr else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs index 47d1f9f177..daa63da702 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -50,8 +50,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { returns, args, .. - }) = stmt else { - return + }) = stmt + else { + return; }; let Some(returns) = returns else { diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index c4272d4a5f..b4e5aa23be 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -392,7 +392,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> let statements = if outer_indent.is_empty() { &mut tree.body } else { - let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body else { + let [Statement::Compound(CompoundStatement::FunctionDef(embedding))] = &mut *tree.body + else { bail!("Expected statement to be embedded in a function definition") }; diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index 11b5582579..01d27636d3 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -481,7 +481,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: returned_id, .. }) = value.as_ref() else { + let Expr::Name(ast::ExprName { + id: returned_id, .. + }) = value.as_ref() + else { continue; }; @@ -494,7 +497,10 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) { continue; }; - let Expr::Name(ast::ExprName { id: assigned_id, .. }) = target else { + let Expr::Name(ast::ExprName { + id: assigned_id, .. + }) = target + else { continue; }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs index 4bf4e77d08..df882a4b0e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_bool_op.rs @@ -299,7 +299,12 @@ fn is_same_expr<'a>(a: &'a Expr, b: &'a Expr) -> Option<&'a str> { /// SIM101 pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _ } )= expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -308,7 +313,13 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { let mut duplicates: FxHashMap> = FxHashMap::default(); for (index, call) in values.iter().enumerate() { // Verify that this is an `isinstance` call. - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = &call else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = &call + else { continue; }; if args.len() != 2 { @@ -430,7 +441,13 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) { } fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _ } )= expr else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = expr + else { return None; }; if ops.len() != 1 || comparators.len() != 1 { @@ -451,7 +468,12 @@ fn match_eq_target(expr: &Expr) -> Option<(&str, &Expr)> { /// SIM109 pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _ }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; @@ -540,7 +562,12 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) { /// SIM220 pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::And, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::And, + values, + range: _, + }) = expr + else { return; }; if values.len() < 2 { @@ -594,7 +621,12 @@ pub(crate) fn expr_and_not_expr(checker: &mut Checker, expr: &Expr) { /// SIM221 pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) { - let Expr::BoolOp(ast::ExprBoolOp { op: BoolOp::Or, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op: BoolOp::Or, + values, + range: _, + }) = expr + else { return; }; if values.len() < 2 { @@ -672,7 +704,12 @@ fn is_short_circuit( expected_op: BoolOp, checker: &Checker, ) -> Option<(Edit, ContentAround)> { - let Expr::BoolOp(ast::ExprBoolOp { op, values, range: _, }) = expr else { + let Expr::BoolOp(ast::ExprBoolOp { + op, + values, + range: _, + }) = expr + else { return None; }; if *op != expected_op { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs index d1a570a0f2..bf9409f4bd 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_expr.rs @@ -109,7 +109,11 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex let Some(arg) = args.get(0) else { return; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), .. }) = arg else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + .. + }) = arg + else { return; }; if !checker @@ -143,7 +147,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else { return; }; - let Expr::Attribute(ast::ExprAttribute { value: attr_value, attr, .. }) = value.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { + value: attr_value, + attr, + .. + }) = value.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = attr_value.as_ref() else { @@ -152,7 +161,12 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { if id != "os" || attr != "environ" { return; } - let Expr::Constant(ast::ExprConstant { value: Constant::Str(env_var), kind, range: _ }) = slice.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(env_var), + kind, + range: _, + }) = slice.as_ref() + else { return; }; let capital_env_var = env_var.to_ascii_uppercase(); @@ -184,13 +198,19 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) { /// SIM910 pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) { - let Expr::Call(ast::ExprCall { func, args, keywords, range: _ }) = expr else { + let Expr::Call(ast::ExprCall { + func, + args, + keywords, + range: _, + }) = expr + else { return; }; if !keywords.is_empty() { return; } - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else { return; }; if !value.is_dict_expr() { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs index 9466328880..0accafcc0f 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_if.rs @@ -300,7 +300,15 @@ fn is_main_check(expr: &Expr) -> bool { /// ... /// ``` fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> { - let [Stmt::If(ast::StmtIf { test, body: inner_body, orelse, .. })] = body else { return None }; + let [Stmt::If(ast::StmtIf { + test, + body: inner_body, + orelse, + .. + })] = body + else { + return None; + }; if !orelse.is_empty() { return None; } @@ -429,10 +437,19 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option { /// SIM103 pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; - let (Some(if_return), Some(else_return)) = (is_one_line_return_bool(body), is_one_line_return_bool(orelse)) else { + let (Some(if_return), Some(else_return)) = ( + is_one_line_return_bool(body), + is_one_line_return_bool(orelse), + ) else { return; }; @@ -515,25 +532,41 @@ fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> /// SIM108 pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ } )= stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_targets, value: body_value, .. } )= &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_targets, + value: body_value, + .. + }) = &body[0] + else { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_targets, value: orelse_value, .. } )= &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_targets, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if body_targets.len() != 1 || orelse_targets.len() != 1 { return; } - let Expr::Name(ast::ExprName { id: body_id, .. } )= &body_targets[0] else { + let Expr::Name(ast::ExprName { id: body_id, .. }) = &body_targets[0] else { return; }; - let Expr::Name(ast::ExprName { id: orelse_id, .. } )= &orelse_targets[0] else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = &orelse_targets[0] else { return; }; if body_id != orelse_id { @@ -638,7 +671,13 @@ fn get_if_body_pairs<'a>( if orelse.len() != 1 { break; } - let Stmt::If(ast::StmtIf { test, body, orelse: orelse_orelse, range: _ }) = &orelse[0] else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse: orelse_orelse, + range: _, + }) = &orelse[0] + else { break; }; pairs.push((test, body)); @@ -649,7 +688,13 @@ fn get_if_body_pairs<'a>( /// SIM114 pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = stmt else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = stmt + else { return; }; @@ -718,7 +763,8 @@ pub(crate) fn manual_dict_lookup( ops, comparators, range: _, - })= &test else { + }) = &test + else { return; }; let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else { @@ -736,7 +782,10 @@ pub(crate) fn manual_dict_lookup( if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. }) = &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { @@ -783,7 +832,13 @@ pub(crate) fn manual_dict_lookup( let mut child: Option<&Stmt> = orelse.get(0); while let Some(current) = child.take() { - let Stmt::If(ast::StmtIf { test, body, orelse, range: _ }) = ¤t else { + let Stmt::If(ast::StmtIf { + test, + body, + orelse, + range: _, + }) = ¤t + else { return; }; if body.len() != 1 { @@ -796,8 +851,9 @@ pub(crate) fn manual_dict_lookup( left, ops, comparators, - range: _ - } )= test.as_ref() else { + range: _, + }) = test.as_ref() + else { return; }; let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else { @@ -809,10 +865,13 @@ pub(crate) fn manual_dict_lookup( if comparators.len() != 1 { return; } - let Expr::Constant(ast::ExprConstant { value: constant, .. } )= &comparators[0] else { + let Expr::Constant(ast::ExprConstant { + value: constant, .. + }) = &comparators[0] + else { return; }; - let Stmt::Return(ast::StmtReturn { value, range: _ } )= &body[0] else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else { return; }; if value.as_ref().map_or(false, |value| { @@ -859,19 +918,35 @@ pub(crate) fn use_dict_get_with_default( if body.len() != 1 || orelse.len() != 1 { return; } - let Stmt::Assign(ast::StmtAssign { targets: body_var, value: body_value, ..}) = &body[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: body_var, + value: body_value, + .. + }) = &body[0] + else { return; }; if body_var.len() != 1 { return; }; - let Stmt::Assign(ast::StmtAssign { targets: orelse_var, value: orelse_value, .. }) = &orelse[0] else { + let Stmt::Assign(ast::StmtAssign { + targets: orelse_var, + value: orelse_value, + .. + }) = &orelse[0] + else { return; }; if orelse_var.len() != 1 { return; }; - let Expr::Compare(ast::ExprCompare { left: test_key, ops , comparators: test_dict, range: _ }) = &test else { + let Expr::Compare(ast::ExprCompare { + left: test_key, + ops, + comparators: test_dict, + range: _, + }) = &test + else { return; }; if test_dict.len() != 1 { @@ -885,7 +960,12 @@ pub(crate) fn use_dict_get_with_default( } }; let test_dict = &test_dict[0]; - let Expr::Subscript(ast::ExprSubscript { value: expected_subscript, slice: expected_slice, .. } ) = expected_value.as_ref() else { + let Expr::Subscript(ast::ExprSubscript { + value: expected_subscript, + slice: expected_slice, + .. + }) = expected_value.as_ref() + else { return; }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs index eee0b0d1d1..d9f15d068e 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_ifexp.rs @@ -141,13 +141,13 @@ pub(crate) fn explicit_true_false_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::Constant(ast::ExprConstant { value, .. } )= &body else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &body else { return; }; if !matches!(value, Constant::Bool(true)) { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &orelse else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &orelse else { return; }; if !matches!(value, Constant::Bool(false)) { @@ -237,7 +237,12 @@ pub(crate) fn twisted_arms_in_ifexpr( body: &Expr, orelse: &Expr, ) { - let Expr::UnaryOp(ast::ExprUnaryOp { op, operand: test_operand, range: _ } )= &test else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op, + operand: test_operand, + range: _, + }) = &test + else { return; }; if !op.is_not() { @@ -245,10 +250,10 @@ pub(crate) fn twisted_arms_in_ifexpr( } // Check if the test operand and else branch use the same variable. - let Expr::Name(ast::ExprName { id: test_id, .. } )= test_operand.as_ref() else { + let Expr::Name(ast::ExprName { id: test_id, .. }) = test_operand.as_ref() else { return; }; - let Expr::Name(ast::ExprName {id: orelse_id, ..}) = orelse else { + let Expr::Name(ast::ExprName { id: orelse_id, .. }) = orelse else { return; }; if !test_id.eq(orelse_id) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs index 05b96f3e19..2febbca3a6 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/ast_unary_op.rs @@ -127,7 +127,13 @@ fn is_dunder_method(name: &str) -> bool { } fn is_exception_check(stmt: &Stmt) -> bool { - let Stmt::If(ast::StmtIf {test: _, body, orelse: _, range: _ })= stmt else { + let Stmt::If(ast::StmtIf { + test: _, + body, + orelse: _, + range: _, + }) = stmt + else { return false; }; if body.len() != 1 { @@ -149,7 +155,13 @@ pub(crate) fn negation_with_equal_op( if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; if !matches!(&ops[..], [CmpOp::Eq]) { @@ -201,7 +213,13 @@ pub(crate) fn negation_with_not_equal_op( if !matches!(op, UnaryOp::Not) { return; } - let Expr::Compare(ast::ExprCompare { left, ops, comparators, range: _}) = operand else { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + range: _, + }) = operand + else { return; }; if !matches!(&ops[..], [CmpOp::NotEq]) { @@ -248,7 +266,12 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o if !matches!(op, UnaryOp::Not) { return; } - let Expr::UnaryOp(ast::ExprUnaryOp { op: operand_op, operand, range: _ }) = operand else { + let Expr::UnaryOp(ast::ExprUnaryOp { + op: operand_op, + operand, + range: _, + }) = operand + else { return; }; if !matches!(operand_op, UnaryOp::Not) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 0988aaf3b3..3199c6c509 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -90,7 +90,8 @@ pub(crate) fn fix_nested_if_statements( body: Suite::IndentedBlock(ref mut outer_body), orelse: None, .. - } = outer_if else { + } = outer_if + else { bail!("Expected outer if to have indented body and no else") }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index b3636cabbc..649496bb8b 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -54,13 +54,12 @@ pub(crate) fn fix_multiple_with_statements( let With { body: Suite::IndentedBlock(ref mut outer_body), .. - } = outer_with else { + } = outer_with + else { bail!("Expected outer with to have indented body") }; - let [Statement::Compound(CompoundStatement::With(inner_with))] = - &mut *outer_body.body - else { + let [Statement::Compound(CompoundStatement::With(inner_with))] = &mut *outer_body.body else { bail!("Expected one inner with statement"); }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index 7bdd4b2365..f4738c06bc 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -71,8 +71,9 @@ fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: TextRang func, args, keywords, - range: _ - }) = &right else { + range: _, + }) = &right + else { return; }; if !(args.is_empty() && keywords.is_empty()) { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs index 98c35c4d15..090d9c3e1d 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs @@ -49,9 +49,9 @@ fn match_async_exit_stack(semantic: &SemanticModel) -> bool { let Expr::Await(ast::ExprAwait { value, range: _ }) = expr else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { - return false; - }; + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { + return false; + }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; @@ -80,9 +80,9 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool { let Some(expr) = semantic.expr_parent() else { return false; }; - let Expr::Call(ast::ExprCall { func, .. }) = expr else { - return false; - }; + let Expr::Call(ast::ExprCall { func, .. }) = expr else { + return false; + }; let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return false; }; diff --git a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs index 340ad9f956..f0e757b111 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/reimplemented_builtin.rs @@ -221,7 +221,8 @@ fn return_values_for_else(stmt: &Stmt) -> Option { iter, orelse, .. - }) = stmt else { + }) = stmt + else { return None; }; @@ -236,8 +237,10 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Stmt::If(ast::StmtIf { body: nested_body, test: nested_test, - orelse: nested_orelse, range: _, - }) = &body[0] else { + orelse: nested_orelse, + range: _, + }) = &body[0] + else { return None; }; if nested_body.len() != 1 { @@ -252,18 +255,30 @@ fn return_values_for_else(stmt: &Stmt) -> Option { let Some(value) = value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(value), .. }) = value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(value), + .. + }) = value.as_ref() + else { return None; }; // The `else` block has to contain a single `return True` or `return False`. - let Stmt::Return(ast::StmtReturn { value: next_value, range: _ }) = &orelse[0] else { + let Stmt::Return(ast::StmtReturn { + value: next_value, + range: _, + }) = &orelse[0] + else { return None; }; let Some(next_value) = next_value else { return None; }; - let Expr::Constant(ast::ExprConstant { value: Constant::Bool(next_value), .. }) = next_value.as_ref() else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Bool(next_value), + .. + }) = next_value.as_ref() + else { return None; }; @@ -286,7 +301,8 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option(stmt: &'a Stmt, sibling: &'a Stmt) -> Option Option { } pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: &str) { - let Expr::Call(ast::ExprCall { - args, - keywords, - .. - }) = expr else { + let Expr::Call(ast::ExprCall { args, keywords, .. }) = expr else { return; }; @@ -111,7 +107,9 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: // Try to build the fstring (internally checks whether e.g. the elements are // convertible to f-string parts). - let Some(new_expr) = build_fstring(joiner, joinees) else { return }; + let Some(new_expr) = build_fstring(joiner, joinees) else { + return; + }; let contents = checker.generator().expr(&new_expr); diff --git a/crates/ruff/src/rules/isort/rules/add_required_imports.rs b/crates/ruff/src/rules/isort/rules/add_required_imports.rs index 8542b4ea9a..9abcb3971c 100644 --- a/crates/ruff/src/rules/isort/rules/add_required_imports.rs +++ b/crates/ruff/src/rules/isort/rules/add_required_imports.rs @@ -56,10 +56,7 @@ impl AlwaysAutofixableViolation for MissingRequiredImport { fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { match target { AnyImport::Import(target) => { - let Stmt::Import(ast::StmtImport { - names, - range: _, - }) = &stmt else { + let Stmt::Import(ast::StmtImport { names, range: _ }) = &stmt else { return false; }; names.iter().any(|alias| { @@ -71,8 +68,9 @@ fn includes_import(stmt: &Stmt, target: &AnyImport) -> bool { module, names, level, - range: _, - }) = &stmt else { + range: _, + }) = &stmt + else { return false; }; module.as_deref() == target.module diff --git a/crates/ruff/src/rules/pandas_vet/rules/call.rs b/crates/ruff/src/rules/pandas_vet/rules/call.rs index be7dfadf24..a0ab67ca2c 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/call.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/call.rs @@ -62,7 +62,7 @@ impl Violation for PandasUseOfDotStack { pub(crate) fn call(checker: &mut Checker, func: &Expr) { let rules = &checker.settings.rules; - let Expr::Attribute(ast::ExprAttribute { value, attr, .. } )= func else { + let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func else { return; }; let violation: DiagnosticKind = match attr.as_str() { diff --git a/crates/ruff/src/rules/pep8_naming/helpers.rs b/crates/ruff/src/rules/pep8_naming/helpers.rs index ac6d369b38..24017389ff 100644 --- a/crates/ruff/src/rules/pep8_naming/helpers.rs +++ b/crates/ruff/src/rules/pep8_naming/helpers.rs @@ -26,7 +26,7 @@ pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) - let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { @@ -41,7 +41,7 @@ pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { @@ -53,7 +53,7 @@ pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> b let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else { return false; }; - let Expr::Call(ast::ExprCall {func, ..}) = value.as_ref() else { + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { return false; }; semantic.resolve_call_path(func).map_or(false, |call_path| { diff --git a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs index c2af990708..17e2d2f93b 100644 --- a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -56,12 +56,8 @@ impl AlwaysAutofixableViolation for IncorrectDictIterator { /// PERF102 pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter: &Expr) { - let Expr::Tuple(ast::ExprTuple { - elts, - .. - }) = target - else { - return + let Expr::Tuple(ast::ExprTuple { elts, .. }) = target else { + return; }; if elts.len() != 2 { return; @@ -72,7 +68,7 @@ pub(crate) fn incorrect_dict_iterator(checker: &mut Checker, target: &Expr, iter if !args.is_empty() { return; } - let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { + let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() else { return; }; if attr != "items" { diff --git a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs index 9eaaea1dc5..900b22cc52 100644 --- a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -48,7 +48,13 @@ impl AlwaysAutofixableViolation for UnnecessaryListCast { /// PERF101 pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { - let Expr::Call(ast::ExprCall{ func, args, range: list_range, ..}) = iter else { + let Expr::Call(ast::ExprCall { + func, + args, + range: list_range, + .. + }) = iter + else { return; }; @@ -56,7 +62,7 @@ pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) { return; } - let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else{ + let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs index 3d4524d497..d13f094e24 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_class.rs @@ -161,10 +161,11 @@ impl AlwaysAutofixableViolation for BlankLineBeforeClass { /// D203, D204, D211 pub(crate) fn blank_before_after_class(checker: &mut Checker, docstring: &Docstring) { let Definition::Member(Member { - kind: MemberKind::Class | MemberKind::NestedClass , + kind: MemberKind::Class | MemberKind::NestedClass, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs index 44bea30595..ccdeed72b7 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/blank_before_after_function.rs @@ -107,7 +107,8 @@ pub(crate) fn blank_before_after_function(checker: &mut Checker, docstring: &Doc kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs index 1e5c110c47..b17ef0b3cf 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/capitalized.rs @@ -69,7 +69,7 @@ pub(crate) fn capitalized(checker: &mut Checker, docstring: &Docstring) { let body = docstring.body(); let Some(first_word) = body.split(' ').next() else { - return + return; }; // Like pydocstyle, we only support ASCII for now. diff --git a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs index 87262ccce1..ee9e2364ea 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/if_needed.rs @@ -84,7 +84,8 @@ pub(crate) fn if_needed(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; if !is_overload(cast::decorator_list(stmt), checker.semantic()) { diff --git a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs index b470da5ea7..7660553e96 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/multi_line_summary_start.rs @@ -132,10 +132,7 @@ pub(crate) fn multi_line_summary_start(checker: &mut Checker, docstring: &Docstr }; let mut content_lines = UniversalNewlineIterator::with_offset(contents, docstring.start()); - let Some(first_line) = content_lines - .next() - else - { + let Some(first_line) = content_lines.next() else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs index 486df63860..11054ab016 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/no_signature.rs @@ -58,7 +58,8 @@ pub(crate) fn no_signature(checker: &mut Checker, docstring: &Docstring) { kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; let Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) = stmt else { diff --git a/crates/ruff/src/rules/pydocstyle/rules/sections.rs b/crates/ruff/src/rules/pydocstyle/rules/sections.rs index eb2676b1c9..4dd426f4b5 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/sections.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/sections.rs @@ -1715,18 +1715,18 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: & kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method, stmt, .. - }) = docstring.definition else { + }) = docstring.definition + else { return; }; - let ( - Stmt::FunctionDef(ast::StmtFunctionDef { - args: arguments, .. - }) - | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - args: arguments, .. - }) - ) = stmt else { + let (Stmt::FunctionDef(ast::StmtFunctionDef { + args: arguments, .. + }) + | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { + args: arguments, .. + })) = stmt + else { return; }; diff --git a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs index eccabe28ec..4e73aa0ed3 100644 --- a/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs +++ b/crates/ruff/src/rules/pydocstyle/rules/starts_with_this.rs @@ -58,7 +58,7 @@ pub(crate) fn starts_with_this(checker: &mut Checker, docstring: &Docstring) { } let Some(first_word) = trimmed.split(' ').next() else { - return + return; }; if normalize_word(first_word) != "this" { return; diff --git a/crates/ruff/src/rules/pyflakes/cformat.rs b/crates/ruff/src/rules/pyflakes/cformat.rs index 1a7c3f6d9e..1f9279c241 100644 --- a/crates/ruff/src/rules/pyflakes/cformat.rs +++ b/crates/ruff/src/rules/pyflakes/cformat.rs @@ -25,8 +25,8 @@ impl From<&CFormatString> for CFormatSummary { ref min_field_width, ref precision, .. - }) = format_part.1 else - { + }) = format_part.1 + else { continue; }; match mapping_key { diff --git a/crates/ruff/src/rules/pyflakes/format.rs b/crates/ruff/src/rules/pyflakes/format.rs index 03d1f30f95..abb52e8c8a 100644 --- a/crates/ruff/src/rules/pyflakes/format.rs +++ b/crates/ruff/src/rules/pyflakes/format.rs @@ -44,7 +44,8 @@ impl TryFrom<&str> for FormatSummary { field_name, format_spec, .. - } = format_part else { + } = format_part + else { continue; }; let parsed = FieldName::parse(field_name)?; diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index 8af22107a3..51d9360558 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -10,18 +10,17 @@ use crate::settings::Settings; pub(super) fn in_dunder_init(semantic: &SemanticModel, settings: &Settings) -> bool { let scope = semantic.scope(); - let ( - ScopeKind::Function(ast::StmtFunctionDef { - name, - decorator_list, + let (ScopeKind::Function(ast::StmtFunctionDef { + name, + decorator_list, .. - }) | - ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { - name, - decorator_list, - .. - }) - ) = scope.kind else { + }) + | ScopeKind::AsyncFunction(ast::StmtAsyncFunctionDef { + name, + decorator_list, + .. + })) = scope.kind + else { return false; }; if name != "__init__" { diff --git a/crates/ruff/src/rules/pylint/rules/import_self.rs b/crates/ruff/src/rules/pylint/rules/import_self.rs index a06f84bfe4..63ba4cfb99 100644 --- a/crates/ruff/src/rules/pylint/rules/import_self.rs +++ b/crates/ruff/src/rules/pylint/rules/import_self.rs @@ -60,7 +60,8 @@ pub(crate) fn import_from_self( let Some(module_path) = module_path else { return None; }; - let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) else { + let Some(imported_module_path) = resolve_imported_module_path(level, module, Some(module_path)) + else { return None; }; diff --git a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs index ccb4113967..b8c78baab8 100644 --- a/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs +++ b/crates/ruff/src/rules/pylint/rules/invalid_envvar_default.rs @@ -94,7 +94,12 @@ pub(crate) fn invalid_envvar_default( let Some(expr) = args.get(1).or_else(|| { keywords .iter() - .find(|keyword| keyword.arg.as_ref().map_or(false, |arg| arg .as_str()== "default")) + .find(|keyword| { + keyword + .arg + .as_ref() + .map_or(false, |arg| arg.as_str() == "default") + }) .map(|keyword| &keyword.value) }) else { return; diff --git a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs index 1ca5ad354a..05cc249fc3 100644 --- a/crates/ruff/src/rules/pylint/rules/nested_min_max.rs +++ b/crates/ruff/src/rules/pylint/rules/nested_min_max.rs @@ -143,7 +143,7 @@ pub(crate) fn nested_min_max( } if args.iter().any(|arg| { - let Expr::Call(ast::ExprCall { func, keywords, ..} )= arg else { + let Expr::Call(ast::ExprCall { func, keywords, .. }) = arg else { return false; }; MinMax::try_from_call(func.as_ref(), keywords.as_ref(), checker.semantic()) == Some(min_max) diff --git a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs index eb02b3cc9f..e77b612130 100644 --- a/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs +++ b/crates/ruff/src/rules/pylint/rules/unexpected_special_method_signature.rs @@ -160,7 +160,9 @@ pub(crate) fn unexpected_special_method_signature( let actual_params = args.args.len(); let mandatory_params = args.args.iter().filter(|arg| arg.default.is_none()).count(); - let Some(expected_params) = ExpectedParams::from_method(name, is_staticmethod(decorator_list, checker.semantic())) else { + let Some(expected_params) = + ExpectedParams::from_method(name, is_staticmethod(decorator_list, checker.semantic())) + else { return; }; diff --git a/crates/ruff/src/rules/pylint/rules/useless_return.rs b/crates/ruff/src/rules/pylint/rules/useless_return.rs index 58b6b1b46e..28e7c689f4 100644 --- a/crates/ruff/src/rules/pylint/rules/useless_return.rs +++ b/crates/ruff/src/rules/pylint/rules/useless_return.rs @@ -85,7 +85,7 @@ pub(crate) fn useless_return<'a>( } // Verify that the last statement is a return statement. - let Stmt::Return(ast::StmtReturn { value, range: _}) = &last_stmt else { + let Stmt::Return(ast::StmtReturn { value, range: _ }) = &last_stmt else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs index 284342608f..63298260cf 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/convert_named_tuple_functional_to_class.rs @@ -81,7 +81,8 @@ fn match_named_tuple_assign<'a>( args, keywords, range: _, - }) = value else { + }) = value + else { return None; }; if !semantic.match_typing_expr(func, "NamedTuple") { @@ -136,10 +137,12 @@ fn match_defaults(keywords: &[Keyword]) -> Result<&[Expr]> { /// Create a list of property assignments from the `NamedTuple` arguments. fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result> { let Some(fields) = args.get(1) else { - let node = Stmt::Pass(ast::StmtPass { range: TextRange::default()}); + let node = Stmt::Pass(ast::StmtPass { + range: TextRange::default(), + }); return Ok(vec![node]); }; - let Expr::List(ast::ExprList { elts, .. } )= &fields else { + let Expr::List(ast::ExprList { elts, .. }) = &fields else { bail!("Expected argument to be `Expr::List`"); }; if elts.is_empty() { @@ -167,7 +170,8 @@ fn create_properties_from_args(args: &[Expr], defaults: &[Expr]) -> Result( func, args, keywords, - range: _ - }) = value else { + range: _, + }) = value + else { return None; }; if !semantic.match_typing_expr(func, "TypedDict") { @@ -205,7 +206,7 @@ fn properties_from_keywords(keywords: &[Keyword]) -> Result> { fn match_total_from_only_keyword(keywords: &[Keyword]) -> Option<&Keyword> { keywords.iter().find(|keyword| { let Some(arg) = &keyword.arg else { - return false + return false; }; arg.as_str() == "total" }) @@ -271,8 +272,8 @@ pub(crate) fn convert_typed_dict_functional_to_class( value: &Expr, ) { let Some((class_name, args, keywords, base_class)) = - match_typed_dict_assign(targets, value, checker.semantic()) else - { + match_typed_dict_assign(targets, value, checker.semantic()) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index 314df7705c..4dac80b3e4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -471,7 +471,7 @@ impl<'a> ImportReplacer<'a> { // line, we can't add a statement after it. For example, if we have // `if True: import foo`, we can't add a statement to the next line. let Some(indentation) = indentation else { - let operation = WithoutRename { + let operation = WithoutRename { target: target.to_string(), members: matched_names .iter() diff --git a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs index 6bdcee9b63..f5d67ae166 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/extraneous_parentheses.rs @@ -106,7 +106,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; match tok { @@ -122,7 +122,7 @@ fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(u if i >= tokens.len() { return None; } - let Ok(( tok, _)) = &tokens[i] else { + let Ok((tok, _)) = &tokens[i] else { return None; }; if matches!(tok, Tok::Rpar) { diff --git a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs index bacfc9c4c2..9420335056 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/f_strings.rs @@ -197,7 +197,7 @@ fn try_convert_to_f_string(expr: &Expr, locator: &Locator) -> Option { return None; }; - let Some(mut summary) = FormatSummaryValues::try_from_expr( expr, locator) else { + let Some(mut summary) = FormatSummaryValues::try_from_expr(expr, locator) else { return None; }; @@ -325,7 +325,7 @@ pub(crate) fn f_strings(checker: &mut Checker, summary: &FormatSummary, expr: &E // Currently, the only issue we know of is in LibCST: // https://github.com/Instagram/LibCST/issues/846 - let Some(mut contents) = try_convert_to_f_string( expr, checker.locator) else { + let Some(mut contents) = try_convert_to_f_string(expr, checker.locator) else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs index 52a38c173d..29da844358 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_with_maxsize_none.rs @@ -64,7 +64,8 @@ pub(crate) fn lru_cache_with_maxsize_none(checker: &mut Checker, decorator_list: args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs index 2b0058fdcf..1a42ab66dc 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs @@ -62,7 +62,8 @@ pub(crate) fn lru_cache_without_parameters(checker: &mut Checker, decorator_list args, keywords, range: _, - }) = &decorator.expression else { + }) = &decorator.expression + else { continue; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs index 365cba7d26..e9265ee090 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/native_literals.rs @@ -88,11 +88,16 @@ pub(crate) fn native_literals( if (id == "str" || id == "bytes") && checker.semantic().is_builtin(id) { let Some(arg) = args.get(0) else { - let mut diagnostic = Diagnostic::new(NativeLiterals{literal_type:if id == "str" { - LiteralType::Str - } else { - LiteralType::Bytes - }}, expr.range()); + let mut diagnostic = Diagnostic::new( + NativeLiterals { + literal_type: if id == "str" { + LiteralType::Str + } else { + LiteralType::Bytes + }, + }, + expr.range(), + ); if checker.patch(diagnostic.kind.rule()) { let constant = if id == "bytes" { Constant::Bytes(vec![]) diff --git a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs index 8fad415be1..e84e0ab64b 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -223,7 +223,11 @@ fn fix_py2_block( let parent = checker.semantic().stmt_parent(); let edit = delete_stmt( stmt, - if matches!(block.leading_token.tok, StartTok::If) { parent } else { None }, + if matches!(block.leading_token.tok, StartTok::If) { + parent + } else { + None + }, checker.locator, checker.indexer, ); @@ -348,7 +352,8 @@ pub(crate) fn outdated_version_block( ops, comparators, range: _, - }) = &test else { + }) = &test + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs index fab3d45868..2362552fad 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/super_call_with_parameters.rs @@ -99,22 +99,27 @@ pub(crate) fn super_call_with_parameters( // Find the enclosing function definition (if any). let Some(Stmt::FunctionDef(ast::StmtFunctionDef { args: parent_args, .. - })) = parents.find(|stmt| stmt.is_function_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_function_def_stmt()) + else { return; }; // Extract the name of the first argument to the enclosing function. let Some(ArgWithDefault { - def: Arg { arg: parent_arg, .. }, + def: Arg { + arg: parent_arg, .. + }, .. - }) = parent_args.args.first() else { + }) = parent_args.args.first() + else { return; }; // Find the enclosing class definition (if any). let Some(Stmt::ClassDef(ast::StmtClassDef { name: parent_name, .. - })) = parents.find(|stmt| stmt.is_class_def_stmt()) else { + })) = parents.find(|stmt| stmt.is_class_def_stmt()) + else { return; }; @@ -125,7 +130,8 @@ pub(crate) fn super_call_with_parameters( Expr::Name(ast::ExprName { id: second_arg_id, .. }), - ) = (first_arg, second_arg) else { + ) = (first_arg, second_arg) + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs index 81e3f40daf..9be33ea2c0 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/type_of_primitive.rs @@ -66,7 +66,7 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr, { return; } - let Expr::Constant(ast::ExprConstant { value, .. } )= &args[0] else { + let Expr::Constant(ast::ExprConstant { value, .. }) = &args[0] else { return; }; let Some(primitive) = Primitive::from_constant(value) else { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index ed48bfc99b..3c021a7e45 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -61,7 +61,8 @@ fn match_encoded_variable(func: &Expr) -> Option<&Expr> { value: variable, attr, .. - }) = func else { + }) = func + else { return None; }; if attr != "encode" { diff --git a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs index 939b5e98bd..193fe237a2 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unpacked_list_comprehension.rs @@ -56,7 +56,8 @@ pub(crate) fn unpacked_list_comprehension(checker: &mut Checker, targets: &[Expr elt, generators, range: _, - }) = value else { + }) = value + else { return; }; diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs index bb54016701..286c4116f1 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_metaclass_type.rs @@ -52,13 +52,13 @@ pub(crate) fn useless_metaclass_type( return; } let Expr::Name(ast::ExprName { id, .. }) = targets.first().unwrap() else { - return ; + return; }; if id != "__metaclass__" { return; } let Expr::Name(ast::ExprName { id, .. }) = value else { - return ; + return; }; if id != "type" { return; diff --git a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs index 367c4ee026..2a9c25725b 100644 --- a/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs +++ b/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs @@ -86,7 +86,13 @@ enum Type { /// Recursively merge all the tuples and lists in the expression. fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> { - let Expr::BinOp(ast::ExprBinOp { left, op: Operator::Add, right, range: _ }) = expr else { + let Expr::BinOp(ast::ExprBinOp { + left, + op: Operator::Add, + right, + range: _, + }) = expr + else { return None; }; @@ -171,7 +177,7 @@ pub(crate) fn collection_literal_concatenation(checker: &mut Checker, expr: &Exp } let Some((new_expr, type_)) = concatenate_expressions(expr) else { - return + return; }; let contents = match type_ { diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index b87135e0bd..419fd6df41 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -82,7 +82,8 @@ pub(crate) fn explicit_f_string_type_conversion( args, keywords, .. - }) = value.as_ref() else { + }) = value.as_ref() + else { continue; }; diff --git a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs index 2ceb04365d..64fb85c073 100644 --- a/crates/ruff/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff/src/rules/ruff/rules/implicit_optional.rs @@ -191,7 +191,7 @@ impl<'a> TypingTarget<'a> { if semantic.match_typing_expr(value, "Optional") { return Some(TypingTarget::Optional); } - let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else{ + let Expr::Tuple(ast::ExprTuple { elts: elements, .. }) = slice.as_ref() else { return None; }; if semantic.match_typing_expr(value, "Literal") { @@ -266,31 +266,41 @@ impl<'a> TypingTarget<'a> { | TypingTarget::Any | TypingTarget::Object => true, TypingTarget::Literal(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; // Literal can only contain `None`, a literal value, other `Literal` // or an enum value. match new_target { TypingTarget::None => true, - TypingTarget::Literal(_) => new_target.contains_none(semantic, locator, target_version), + TypingTarget::Literal(_) => { + new_target.contains_none(semantic, locator, target_version) + } _ => false, } }), TypingTarget::Union(elements) => elements.iter().any(|element| { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) }), TypingTarget::Annotated(element) => { - let Some(new_target) = TypingTarget::try_from_expr(element, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(element, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) } TypingTarget::ForwardReference(expr) => { - let Some(new_target) = TypingTarget::try_from_expr(expr, semantic, locator, target_version) else { + let Some(new_target) = + TypingTarget::try_from_expr(expr, semantic, locator, target_version) + else { return false; }; new_target.contains_none(semantic, locator, target_version) @@ -312,7 +322,8 @@ fn type_hint_explicitly_allows_none<'a>( locator: &Locator, target_version: PythonVersion, ) -> Option<&'a Expr> { - let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) else { + let Some(target) = TypingTarget::try_from_expr(annotation, semantic, locator, target_version) + else { return Some(annotation); }; match target { @@ -392,14 +403,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { .chain(&arguments.args) .chain(&arguments.kwonlyargs) { - let Some(default) = default else { - continue - }; + let Some(default) = default else { continue }; if !is_const_none(default) { continue; } let Some(annotation) = &def.annotation else { - continue + continue; }; if let Expr::Constant(ast::ExprConstant { @@ -410,7 +419,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { { // Quoted annotation. if let Ok((annotation, kind)) = parse_type_annotation(string, *range, checker.locator) { - let Some(expr) = type_hint_explicitly_allows_none(&annotation, checker.semantic(), checker.locator, checker.settings.target_version) else { + let Some(expr) = type_hint_explicitly_allows_none( + &annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version, + ) else { continue; }; let conversion_type = checker.settings.target_version.into(); @@ -426,7 +440,12 @@ pub(crate) fn implicit_optional(checker: &mut Checker, arguments: &Arguments) { } } else { // Unquoted annotation. - let Some(expr) = type_hint_explicitly_allows_none(annotation, checker.semantic(), checker.locator, checker.settings.target_version) else { + let Some(expr) = type_hint_explicitly_allows_none( + annotation, + checker.semantic(), + checker.locator, + checker.settings.target_version, + ) else { continue; }; let conversion_type = checker.settings.target_version.into(); diff --git a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs index 657a1638aa..1b3f234a78 100644 --- a/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs +++ b/crates/ruff/src/rules/ruff/rules/pairwise_over_zipped.rs @@ -67,7 +67,7 @@ fn match_slice_info(expr: &Expr) -> Option { return None; }; - let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { + let Expr::Slice(ast::ExprSlice { lower, step, .. }) = slice.as_ref() else { return None; }; diff --git a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs index db26e7588f..f256ff0f91 100644 --- a/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs +++ b/crates/ruff/src/rules/tryceratops/rules/useless_try_except.rs @@ -44,7 +44,10 @@ pub(crate) fn useless_try_except(checker: &mut Checker, handlers: &[ExceptHandle .map(|handler| { let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { name, body, .. }) = handler; - let Some(Stmt::Raise(ast::StmtRaise { exc, cause: None, .. })) = &body.first() else { + let Some(Stmt::Raise(ast::StmtRaise { + exc, cause: None, .. + })) = &body.first() + else { return None; }; if let Some(expr) = exc { diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index 8f91668be0..52f8a65dc1 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -23,7 +23,9 @@ pub(crate) fn show_settings( let Some(entry) = paths .iter() .flatten() - .sorted_by(|a, b| a.path().cmp(b.path())).next() else { + .sorted_by(|a, b| a.path().cmp(b.path())) + .next() + else { bail!("No files found under the given path"); }; let path = entry.path(); diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 4e6bfe280f..4e9c5fe681 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -46,18 +46,24 @@ pub(crate) fn generate() -> String { // Generate all the top-level fields. for (name, entry) in &sorted_options { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, None); output.push_str("---\n\n"); } // Generate all the sub-groups. for (group_name, entry) in &sorted_options { - let OptionEntry::Group(fields) = entry else { continue; }; + let OptionEntry::Group(fields) = entry else { + continue; + }; output.push_str(&format!("### `{group_name}`\n")); output.push('\n'); for (name, entry) in fields.iter().sorted_by_key(|(name, _)| name) { - let OptionEntry::Field(field) = entry else { continue; }; + let OptionEntry::Field(field) = entry else { + continue; + }; emit_field(&mut output, name, field, Some(group_name)); output.push_str("---\n\n"); } diff --git a/crates/ruff_macros/src/derive_message_formats.rs b/crates/ruff_macros/src/derive_message_formats.rs index f72113c223..c155ffb00b 100644 --- a/crates/ruff_macros/src/derive_message_formats.rs +++ b/crates/ruff_macros/src/derive_message_formats.rs @@ -19,7 +19,9 @@ pub(crate) fn derive_message_formats(func: &ItemFn) -> TokenStream { } fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> { - let Some(Stmt::Expr(last, _)) = block.stmts.last() else {panic!("expected last statement in block to be an expression")}; + let Some(Stmt::Expr(last, _)) = block.stmts.last() else { + panic!("expected last statement in block to be an expression") + }; parse_expr(last, strings)?; Ok(()) } @@ -28,7 +30,9 @@ fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> match expr { Expr::Macro(mac) if mac.mac.path.is_ident("format") => { let Some(first_token) = mac.mac.tokens.to_token_stream().into_iter().next() else { - return Err(quote_spanned!(expr.span() => compile_error!("expected format! to have an argument"))) + return Err( + quote_spanned!(expr.span() => compile_error!("expected format! to have an argument")), + ); }; strings.extend(quote! {#first_token,}); Ok(()) diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index 94fbcbdac5..d37113e0aa 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -31,14 +31,30 @@ struct Rule { pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { let Some(last_stmt) = func.block.stmts.last() else { - return Err(Error::new(func.block.span(), "expected body to end in an expression")); + return Err(Error::new( + func.block.span(), + "expected body to end in an expression", + )); }; - let Stmt::Expr(Expr::Call(ExprCall { args: some_args, .. }), _) = last_stmt else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let Stmt::Expr( + Expr::Call(ExprCall { + args: some_args, .. + }), + _, + ) = last_stmt + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; let mut some_args = some_args.into_iter(); - let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) else { - return Err(Error::new(last_stmt.span(), "expected last expression to be `Some(match (..) { .. })`")); + let (Some(Expr::Match(ExprMatch { arms, .. })), None) = (some_args.next(), some_args.next()) + else { + return Err(Error::new( + last_stmt.span(), + "expected last expression to be `Some(match (..) { .. })`", + )); }; // Map from: linter (e.g., `Flake8Bugbear`) to rule code (e.g.,`"002"`) to rule data (e.g., diff --git a/crates/ruff_macros/src/rule_namespace.rs b/crates/ruff_macros/src/rule_namespace.rs index f1bd7f5adf..811033f8ea 100644 --- a/crates/ruff_macros/src/rule_namespace.rs +++ b/crates/ruff_macros/src/rule_namespace.rs @@ -6,10 +6,16 @@ use syn::spanned::Spanned; use syn::{Attribute, Data, DataEnum, DeriveInput, Error, ExprLit, Lit, Meta, MetaNameValue}; pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { - let DeriveInput { ident, data: Data::Enum(DataEnum { - variants, .. - }), .. } = input else { - return Err(Error::new(input.ident.span(), "only named fields are supported")); + let DeriveInput { + ident, + data: Data::Enum(DataEnum { variants, .. }), + .. + } = input + else { + return Err(Error::new( + input.ident.span(), + "only named fields are supported", + )); }; let mut parsed = Vec::new(); @@ -53,8 +59,12 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result syn::Result syn::Result<(String, String)> { - let Meta::NameValue(MetaNameValue{value: syn::Expr::Lit(ExprLit { lit: Lit::Str(doc_lit), ..}), ..}) = &doc_attr.meta else { - return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of #[doc = "..."]"#)) + let Meta::NameValue(MetaNameValue { + value: + syn::Expr::Lit(ExprLit { + lit: Lit::Str(doc_lit), + .. + }), + .. + }) = &doc_attr.meta + else { + return Err(Error::new( + doc_attr.span(), + r#"expected doc attribute to be in the form of #[doc = "..."]"#, + )); }; parse_markdown_link(doc_lit.value().trim()) .map(|(name, url)| (name.to_string(), url.to_string())) diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 1c6c65e0d5..486f7b29ad 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -1411,11 +1411,18 @@ impl Truthiness { Constant::Ellipsis => Some(true), Constant::Tuple(elts) => Some(!elts.is_empty()), }, - Expr::JoinedStr(ast::ExprJoinedStr { values, range: _range }) => { + Expr::JoinedStr(ast::ExprJoinedStr { + values, + range: _range, + }) => { if values.is_empty() { Some(false) } else if values.iter().any(|value| { - let Expr::Constant(ast::ExprConstant { value: Constant::Str(string), .. } )= &value else { + let Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) = &value + else { return false; }; !string.is_empty() @@ -1425,14 +1432,30 @@ impl Truthiness { None } } - Expr::List(ast::ExprList { elts, range: _range, .. }) - | Expr::Set(ast::ExprSet { elts, range: _range }) - | Expr::Tuple(ast::ExprTuple { elts, range: _range,.. }) => Some(!elts.is_empty()), - Expr::Dict(ast::ExprDict { keys, range: _range, .. }) => Some(!keys.is_empty()), + Expr::List(ast::ExprList { + elts, + range: _range, + .. + }) + | Expr::Set(ast::ExprSet { + elts, + range: _range, + }) + | Expr::Tuple(ast::ExprTuple { + elts, + range: _range, + .. + }) => Some(!elts.is_empty()), + Expr::Dict(ast::ExprDict { + keys, + range: _range, + .. + }) => Some(!keys.is_empty()), Expr::Call(ast::ExprCall { func, args, - keywords, range: _range, + keywords, + range: _range, }) => { if let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() { if is_iterable_initializer(id.as_str(), |id| is_builtin(id)) { diff --git a/crates/ruff_python_ast/src/identifier.rs b/crates/ruff_python_ast/src/identifier.rs index 4af761ae83..d8ddc8606c 100644 --- a/crates/ruff_python_ast/src/identifier.rs +++ b/crates/ruff_python_ast/src/identifier.rs @@ -187,8 +187,9 @@ pub fn except(handler: &ExceptHandler, locator: &Locator) -> TextRange { /// Return the [`TextRange`] of the `else` token in a `For`, `AsyncFor`, or `While` statement. pub fn else_(stmt: &Stmt, locator: &Locator) -> Option { let (Stmt::For(ast::StmtFor { body, orelse, .. }) - | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) - | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt else { + | Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. }) + | Stmt::While(ast::StmtWhile { body, orelse, .. })) = stmt + else { return None; }; diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 9e31caddbb..2692b0e27d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -67,14 +67,12 @@ fn handle_match_comment<'a>( // Get the enclosing match case let Some(match_case) = comment.enclosing_node().match_case() else { - return CommentPlacement::Default(comment) + return CommentPlacement::Default(comment); }; // And its parent match statement. - let Some(match_stmt) = comment - .enclosing_parent() - .and_then(AnyNodeRef::stmt_match) else { - return CommentPlacement::Default(comment) + let Some(match_stmt) = comment.enclosing_parent().and_then(AnyNodeRef::stmt_match) else { + return CommentPlacement::Default(comment); }; // Get the next sibling (sibling traversal would be really nice) @@ -163,7 +161,9 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme return CommentPlacement::Default(comment); } - let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + let (Some(AnyNodeRef::ExceptHandlerExceptHandler(preceding_except_handler)), Some(following)) = + (comment.preceding_node(), comment.following_node()) + else { return CommentPlacement::Default(comment); }; @@ -175,10 +175,10 @@ fn handle_in_between_except_handlers_or_except_handler_and_else_or_finally_comme .unwrap_or_default(); let Some(except_indentation) = - whitespace::indentation(locator, preceding_except_handler).map(str::len) else - { - return CommentPlacement::Default(comment); - }; + whitespace::indentation(locator, preceding_except_handler).map(str::len) + else { + return CommentPlacement::Default(comment); + }; if comment_indentation > except_indentation { // Delegate to `handle_trailing_body_comment` @@ -447,7 +447,9 @@ fn handle_trailing_body_comment<'a>( return CommentPlacement::Default(comment); }; - let Some(comment_indentation) = whitespace::indentation_at_offset(locator, comment.slice().range().start()) else { + let Some(comment_indentation) = + whitespace::indentation_at_offset(locator, comment.slice().range().start()) + else { // The comment can't be a comment for the previous block if it isn't indented.. return CommentPlacement::Default(comment); }; @@ -465,7 +467,9 @@ fn handle_trailing_body_comment<'a>( // # Trailing if comment // ``` // Here we keep the comment a trailing comment of the `if` - let Some(preceding_node_indentation) = whitespace::indentation_at_offset(locator, preceding_node.start()) else { + let Some(preceding_node_indentation) = + whitespace::indentation_at_offset(locator, preceding_node.start()) + else { return CommentPlacement::Default(comment); }; if comment_indentation_len == preceding_node_indentation.len() { @@ -593,7 +597,8 @@ fn handle_trailing_end_of_line_condition_comment<'a>( } // Must be between the condition expression and the first body element - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { return CommentPlacement::Default(comment); }; @@ -881,8 +886,9 @@ fn handle_module_level_own_line_comment_before_class_or_function_comment<'a>( } // ... for comments with a preceding and following node, - let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { - return CommentPlacement::Default(comment) + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + else { + return CommentPlacement::Default(comment); }; // ... where the following is a function or class statement. diff --git a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs index e9323cf712..8fb9b42eb2 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bool_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bool_op.rs @@ -49,7 +49,7 @@ impl<'ast> FormatBinaryLike<'ast> for ExprBoolOp { let comments = f.context().comments().clone(); let Some(first) = values.next() else { - return Ok(()) + return Ok(()); }; write!(f, [group(&first.format())])?; diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index d16576a6db..be25b63399 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -52,7 +52,7 @@ impl FormatRule> for FormatSuite { let mut iter = statements.iter(); let Some(first) = iter.next() else { - return Ok(()) + return Ok(()); }; // First entry has never any separator, doesn't matter which one we take; diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index 942e75d341..86516170a1 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -245,7 +245,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.offset), - } + }; }; if self.bogus { @@ -310,7 +310,7 @@ impl<'a> SimpleTokenizer<'a> { return Token { kind: TokenKind::EndOfFile, range: TextRange::empty(self.back_offset), - } + }; }; if self.bogus { diff --git a/crates/ruff_python_resolver/src/implicit_imports.rs b/crates/ruff_python_resolver/src/implicit_imports.rs index 94bf9a9f2c..693b6572ca 100644 --- a/crates/ruff_python_resolver/src/implicit_imports.rs +++ b/crates/ruff_python_resolver/src/implicit_imports.rs @@ -80,7 +80,7 @@ impl ImplicitImports { continue; }; - let Some(name) = path.file_name().and_then(OsStr::to_str) else { + let Some(name) = path.file_name().and_then(OsStr::to_str) else { continue; }; submodules.insert( diff --git a/crates/ruff_python_semantic/src/analyze/logging.rs b/crates/ruff_python_semantic/src/analyze/logging.rs index a96ebad213..48fbc3fb16 100644 --- a/crates/ruff_python_semantic/src/analyze/logging.rs +++ b/crates/ruff_python_semantic/src/analyze/logging.rs @@ -20,7 +20,11 @@ use crate::model::SemanticModel; pub fn is_logger_candidate(func: &Expr, semantic: &SemanticModel) -> bool { if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func { let Some(call_path) = (if let Some(call_path) = semantic.resolve_call_path(value) { - if call_path.first().map_or(false, |module| *module == "logging") || call_path.as_slice() == ["flask", "current_app", "logger"] { + if call_path + .first() + .map_or(false, |module| *module == "logging") + || call_path.as_slice() == ["flask", "current_app", "logger"] + { Some(call_path) } else { None From 0bff4ed4d379cca6d9f549a4fcb9d7a2c48061b1 Mon Sep 17 00:00:00 2001 From: Justin Prieto Date: Sun, 2 Jul 2023 23:52:16 -0400 Subject: [PATCH 07/27] [`flake8-pyi`] Implement PYI002, PYI003, PYI004, PYI005 (#5457) ## Summary Implements flake8-pyi checks 002, 003, 004, 005. The logic is a bit complex, as you can see in the [original code](https://github.com/PyCQA/flake8-pyi/blob/57921813c1fb5b92f810a57753850c212fcc29b0/pyi.py#L1403C18-L1403C18). ref: #848 ## Test Plan Updated snapshot tests. Ran flake8 to double check lints, and ran ruff with all PYI lints enabled to check for incorrect overlapping lint errors. --- .../test/fixtures/flake8_pyi/PYI002.py | 17 + .../test/fixtures/flake8_pyi/PYI002.pyi | 17 + .../test/fixtures/flake8_pyi/PYI003.py | 31 ++ .../test/fixtures/flake8_pyi/PYI003.pyi | 31 ++ .../test/fixtures/flake8_pyi/PYI004.py | 15 + .../test/fixtures/flake8_pyi/PYI004.pyi | 15 + .../test/fixtures/flake8_pyi/PYI005.py | 14 + .../test/fixtures/flake8_pyi/PYI005.pyi | 14 + crates/ruff/src/checkers/ast/mod.rs | 10 + crates/ruff/src/codes.rs | 4 + crates/ruff/src/registry/rule_set.rs | 2 +- crates/ruff/src/rules/flake8_pyi/mod.rs | 8 + .../rules/bad_version_info_comparison.rs | 10 +- crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 2 + .../rules/flake8_pyi/rules/version_info.rs | 357 ++++++++++++++++++ ...__flake8_pyi__tests__PYI002_PYI002.py.snap | 4 + ..._flake8_pyi__tests__PYI002_PYI002.pyi.snap | 72 ++++ ...__flake8_pyi__tests__PYI003_PYI003.py.snap | 4 + ..._flake8_pyi__tests__PYI003_PYI003.pyi.snap | 173 +++++++++ ...__flake8_pyi__tests__PYI004_PYI004.py.snap | 4 + ..._flake8_pyi__tests__PYI004_PYI004.pyi.snap | 42 +++ ...__flake8_pyi__tests__PYI005_PYI005.py.snap | 4 + ..._flake8_pyi__tests__PYI005_PYI005.pyi.snap | 20 + ..._flake8_pyi__tests__PYI006_PYI006.pyi.snap | 12 +- ruff.schema.json | 4 + 25 files changed, 875 insertions(+), 11 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/version_info.rs create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap create mode 100644 crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py new file mode 100644 index 0000000000..857b029cdc --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py @@ -0,0 +1,17 @@ +import sys +from sys import platform, version_info + +if sys.version == 'Python 2.7.10': ... # PYI002 +if 'linux' == sys.platform: ... # PYI002 +if hasattr(sys, 'maxint'): ... # PYI002 +if sys.maxsize == 42: ... # PYI002 +if (2, 7) < sys.version_info < (3, 5): ... # PYI002 +if sys.version[0] == 'P': ... # PYI002 +if False: ... # PYI002 + +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi new file mode 100644 index 0000000000..857b029cdc --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi @@ -0,0 +1,17 @@ +import sys +from sys import platform, version_info + +if sys.version == 'Python 2.7.10': ... # PYI002 +if 'linux' == sys.platform: ... # PYI002 +if hasattr(sys, 'maxint'): ... # PYI002 +if sys.maxsize == 42: ... # PYI002 +if (2, 7) < sys.version_info < (3, 5): ... # PYI002 +if sys.version[0] == 'P': ... # PYI002 +if False: ... # PYI002 + +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.py @@ -0,0 +1,31 @@ +import sys + +if sys.version_info[0] == 2: ... +if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2,): ... +if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons +if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info < (3, 5): ... +if sys.version_info >= (3, 5): ... +if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi new file mode 100644 index 0000000000..9c4481179f --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI003.pyi @@ -0,0 +1,31 @@ +import sys + +if sys.version_info[0] == 2: ... +if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2,): ... +if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons +if sys.version_info > (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info <= (3, 0): ... # Y006 Use only < and >= for version comparisons +if sys.version_info < (3, 5): ... +if sys.version_info >= (3, 5): ... +if (2, 7) <= sys.version_info < (3, 5): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.py @@ -0,0 +1,15 @@ +import sys +from sys import version_info + +if sys.version_info >= (3, 4, 3): ... # PYI004 +if sys.version_info < (3, 4, 3): ... # PYI004 +if sys.version_info == (3, 4, 3): ... # PYI004 +if sys.version_info != (3, 4, 3): ... # PYI004 + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if sys.platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi new file mode 100644 index 0000000000..bca3f9f7e3 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI004.pyi @@ -0,0 +1,15 @@ +import sys +from sys import version_info + +if sys.version_info >= (3, 4, 3): ... # PYI004 +if sys.version_info < (3, 4, 3): ... # PYI004 +if sys.version_info == (3, 4, 3): ... # PYI004 +if sys.version_info != (3, 4, 3): ... # PYI004 + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if sys.platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.py @@ -0,0 +1,14 @@ +import sys +from sys import platform, version_info + +if sys.version_info[:1] == (2, 7): ... # Y005 +if sys.version_info[:2] == (2,): ... # Y005 + + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi new file mode 100644 index 0000000000..0053e9e9df --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI005.pyi @@ -0,0 +1,14 @@ +import sys +from sys import platform, version_info + +if sys.version_info[:1] == (2, 7): ... # Y005 +if sys.version_info[:2] == (2,): ... # Y005 + + +if sys.version_info[0] == 2: ... +if version_info[0] == 2: ... +if sys.version_info < (3, 5): ... +if version_info >= (3, 5): ... +if sys.version_info[:2] == (2, 7): ... +if sys.version_info[:1] == (2,): ... +if platform == 'linux': ... diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 97a2b0451f..0c9f892cce 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1371,6 +1371,16 @@ where self.diagnostics.push(diagnostic); } } + if self.is_stub { + if self.any_enabled(&[ + Rule::ComplexIfStatementInStub, + Rule::UnrecognizedVersionInfoCheck, + Rule::PatchVersionComparison, + Rule::WrongTupleLengthVersionComparison, + ]) { + flake8_pyi::rules::version_info(self, test); + } + } } Stmt::Assert(ast::StmtAssert { test, diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index cc25933e8c..06adeba95b 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -596,6 +596,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { // flake8-pyi (Flake8Pyi, "001") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnprefixedTypeParam), + (Flake8Pyi, "002") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexIfStatementInStub), + (Flake8Pyi, "003") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedVersionInfoCheck), + (Flake8Pyi, "004") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::PatchVersionComparison), + (Flake8Pyi, "005") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::WrongTupleLengthVersionComparison), (Flake8Pyi, "006") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::BadVersionInfoComparison), (Flake8Pyi, "007") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformCheck), (Flake8Pyi, "008") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnrecognizedPlatformName), diff --git a/crates/ruff/src/registry/rule_set.rs b/crates/ruff/src/registry/rule_set.rs index 97e7ac7f3b..555ba0e5b2 100644 --- a/crates/ruff/src/registry/rule_set.rs +++ b/crates/ruff/src/registry/rule_set.rs @@ -3,7 +3,7 @@ use ruff_macros::CacheKey; use std::fmt::{Debug, Formatter}; use std::iter::FusedIterator; -const RULESET_SIZE: usize = 10; +const RULESET_SIZE: usize = 11; /// A set of [`Rule`]s. /// diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index b790c3c77e..5575a7ced5 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -22,6 +22,8 @@ mod tests { #[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))] #[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))] + #[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))] #[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))] #[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))] @@ -56,6 +58,8 @@ mod tests { #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.pyi"))] #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.py"))] #[test_case(Rule::FutureAnnotationsInStub, Path::new("PYI044.pyi"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.py"))] + #[test_case(Rule::PatchVersionComparison, Path::new("PYI004.pyi"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.py"))] #[test_case(Rule::TypeCommentInStub, Path::new("PYI033.pyi"))] #[test_case(Rule::TypedArgumentDefaultInStub, Path::new("PYI011.py"))] @@ -72,6 +76,10 @@ mod tests { #[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))] #[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.py"))] + #[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))] + #[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 8d43070991..18a8d4e1b1 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -52,7 +52,7 @@ pub struct BadVersionInfoComparison; impl Violation for BadVersionInfoComparison { #[derive_message_formats] fn message(&self) -> String { - format!("Use `<` or `>=` for version info comparisons") + format!("Use `<` or `>=` for `sys.version_info` comparisons") } } @@ -78,8 +78,10 @@ pub(crate) fn bad_version_info_comparison( return; } - if !matches!(op, CmpOp::Lt | CmpOp::GtE) { - let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); - checker.diagnostics.push(diagnostic); + if matches!(op, CmpOp::Lt | CmpOp::GtE) { + return; } + + let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); + checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 6286ebdca3..ed96285dcd 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -22,6 +22,7 @@ pub(crate) use type_alias_naming::*; pub(crate) use type_comment_in_stub::*; pub(crate) use unaliased_collections_abc_set_import::*; pub(crate) use unrecognized_platform::*; +pub(crate) use version_info::*; mod any_eq_ne_annotation; mod bad_version_info_comparison; @@ -47,3 +48,4 @@ mod type_alias_naming; mod type_comment_in_stub; mod unaliased_collections_abc_set_import; mod unrecognized_platform; +mod version_info; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/version_info.rs b/crates/ruff/src/rules/flake8_pyi/rules/version_info.rs new file mode 100644 index 0000000000..4e2e2d21de --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/version_info.rs @@ -0,0 +1,357 @@ +use num_bigint::BigInt; +use num_traits::{One, Zero}; +use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; +use smallvec::SmallVec; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; +use crate::registry::Rule; + +/// ## What it does +/// Checks for `if` statements with complex conditionals in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. However, type checkers only understand a limited +/// subset of these conditionals; complex conditionals may result in false +/// positives or false negatives. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if (2, 7) < sys.version_info < (3, 5): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 5): +/// ... +/// ``` +#[violation] +pub struct ComplexIfStatementInStub; + +impl Violation for ComplexIfStatementInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "`if`` test must be a simple comparison against `sys.platform` or `sys.version_info`" + ) + } +} + +/// ## What it does +/// Checks for problematic `sys.version_info`-related conditions in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions using `sys.version_info`. However, there are a number of common +/// mistakes involving `sys.version_info` comparisons that should be avoided. +/// For example, comparing against a string can lead to unexpected behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[0] == "2": +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 2: +/// ... +/// ``` +#[violation] +pub struct UnrecognizedVersionInfoCheck; + +impl Violation for UnrecognizedVersionInfoCheck { + #[derive_message_formats] + fn message(&self) -> String { + format!("Unrecognized `sys.version_info` check") + } +} + +/// ## What it does +/// Checks for Python version comparisons in stubs that compare against patch +/// versions (e.g., Python 3.8.3) instead of major and minor versions (e.g., +/// Python 3.8). +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. However, type checkers only understand a limited +/// subset of these conditionals. In particular, type checkers don't support +/// patch versions (e.g., Python 3.8.3), only major and minor versions (e.g., +/// Python 3.8). Therefore, version checks in stubs should only use the major +/// and minor versions. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4, 3): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info >= (3, 4): +/// ... +/// ``` +#[violation] +pub struct PatchVersionComparison; + +impl Violation for PatchVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + format!("Version comparison must use only major and minor version") + } +} + +/// ## What it does +/// Checks for Python version comparisons that compare against a tuple of the +/// wrong length. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. When comparing against `sys.version_info`, avoid +/// comparing against tuples of the wrong length, which can lead to unexpected +/// behavior. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if sys.version_info[:2] == (3,): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info[0] == 3: +/// ... +/// ``` +#[violation] +pub struct WrongTupleLengthVersionComparison { + expected_length: usize, +} + +impl Violation for WrongTupleLengthVersionComparison { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Version comparison must be against a length-{} tuple.", + self.expected_length + ) + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ExpectedComparator { + MajorDigit, + MajorTuple, + MajorMinorTuple, + AnyTuple, +} + +/// PYI002, PYI003, PYI004, PYI005 +pub(crate) fn version_info(checker: &mut Checker, test: &Expr) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test { + for value in values { + version_info(checker, value); + } + return; + } + + let Some((left, op, comparator, is_platform)) = compare_expr_components(checker, test) else { + if checker.enabled(Rule::ComplexIfStatementInStub) { + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); + } + return; + }; + + // Already covered by PYI007. + if is_platform { + return; + } + + let Ok(expected_comparator) = ExpectedComparator::try_from(left) else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + return; + }; + + check_version_check(checker, expected_comparator, test, op, comparator); +} + +/// Extracts relevant components of the if test. +fn compare_expr_components<'a>( + checker: &Checker, + test: &'a Expr, +) -> Option<(&'a Expr, CmpOp, &'a Expr, bool)> { + test.as_compare_expr().and_then(|cmp| { + let ast::ExprCompare { + left, + ops, + comparators, + .. + } = cmp; + + if comparators.len() != 1 { + return None; + } + + let name_expr = if let Expr::Subscript(ast::ExprSubscript { value, .. }) = left.as_ref() { + value + } else { + left + }; + + // The only valid comparisons are against sys.platform and sys.version_info. + let is_platform = match checker + .semantic() + .resolve_call_path(name_expr) + .as_ref() + .map(SmallVec::as_slice) + { + Some(["sys", "platform"]) => true, + Some(["sys", "version_info"]) => false, + _ => return None, + }; + + Some((left.as_ref(), ops[0], &comparators[0], is_platform)) + }) +} + +fn check_version_check( + checker: &mut Checker, + expected_comparator: ExpectedComparator, + test: &Expr, + op: CmpOp, + comparator: &Expr, +) { + // Single digit comparison, e.g., `sys.version_info[0] == 2`. + if expected_comparator == ExpectedComparator::MajorDigit { + if !is_int_constant(comparator) { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } + return; + } + + // Tuple comparison, e.g., `sys.version_info == (3, 4)`. + let Expr::Tuple(ast::ExprTuple { elts, .. }) = comparator else { + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + return; + }; + + if !elts.iter().all(is_int_constant) { + // All tuple elements must be integers, e.g., `sys.version_info == (3, 4)` instead of + // `sys.version_info == (3.0, 4)`. + if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); + } + } else if elts.len() > 2 { + // Must compare against major and minor version only, e.g., `sys.version_info == (3, 4)` + // instead of `sys.version_info == (3, 4, 0)`. + if checker.enabled(Rule::PatchVersionComparison) { + checker + .diagnostics + .push(Diagnostic::new(PatchVersionComparison, test.range())); + } + } + + if checker.enabled(Rule::WrongTupleLengthVersionComparison) { + if op == CmpOp::Eq || op == CmpOp::NotEq { + let expected_length = match expected_comparator { + ExpectedComparator::MajorTuple => 1, + ExpectedComparator::MajorMinorTuple => 2, + _ => return, + }; + + if elts.len() != expected_length { + checker.diagnostics.push(Diagnostic::new( + WrongTupleLengthVersionComparison { expected_length }, + test.range(), + )); + } + } + } +} + +impl TryFrom<&Expr> for ExpectedComparator { + type Error = (); + + fn try_from(value: &Expr) -> Result { + let Expr::Subscript(ast::ExprSubscript { slice, .. }) = value else { + return Ok(ExpectedComparator::AnyTuple) + }; + + // Only allow simple slices of the form [:n] or explicit indexing into the first element + match slice.as_ref() { + Expr::Slice(ast::ExprSlice { + lower: None, + upper: Some(n), + step: None, + .. + }) => { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Int(n), + .. + }) = n.as_ref() + { + if *n == BigInt::one() { + return Ok(ExpectedComparator::MajorTuple); + } + if *n == BigInt::from(2) { + return Ok(ExpectedComparator::MajorMinorTuple); + } + } + } + Expr::Constant(ast::ExprConstant { + value: Constant::Int(n), + .. + }) if n.is_zero() => { + return Ok(ExpectedComparator::MajorDigit); + } + _ => (), + } + + Err(()) + } +} + +fn is_int_constant(expr: &Expr) -> bool { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: ast::Constant::Int(_), + .. + }) + ) +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap new file mode 100644 index 0000000000..bfac1f9b31 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap @@ -0,0 +1,72 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI002.pyi:4:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +2 | from sys import platform, version_info +3 | +4 | if sys.version == 'Python 2.7.10': ... # PYI002 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +5 | if 'linux' == sys.platform: ... # PYI002 +6 | if hasattr(sys, 'maxint'): ... # PYI002 + | + +PYI002.pyi:5:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +4 | if sys.version == 'Python 2.7.10': ... # PYI002 +5 | if 'linux' == sys.platform: ... # PYI002 + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +6 | if hasattr(sys, 'maxint'): ... # PYI002 +7 | if sys.maxsize == 42: ... # PYI002 + | + +PYI002.pyi:6:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +4 | if sys.version == 'Python 2.7.10': ... # PYI002 +5 | if 'linux' == sys.platform: ... # PYI002 +6 | if hasattr(sys, 'maxint'): ... # PYI002 + | ^^^^^^^^^^^^^^^^^^^^^^ PYI002 +7 | if sys.maxsize == 42: ... # PYI002 +8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 + | + +PYI002.pyi:7:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +5 | if 'linux' == sys.platform: ... # PYI002 +6 | if hasattr(sys, 'maxint'): ... # PYI002 +7 | if sys.maxsize == 42: ... # PYI002 + | ^^^^^^^^^^^^^^^^^ PYI002 +8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 +9 | if sys.version[0] == 'P': ... # PYI002 + | + +PYI002.pyi:8:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | + 6 | if hasattr(sys, 'maxint'): ... # PYI002 + 7 | if sys.maxsize == 42: ... # PYI002 + 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 + 9 | if sys.version[0] == 'P': ... # PYI002 +10 | if False: ... # PYI002 + | + +PYI002.pyi:9:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | + 7 | if sys.maxsize == 42: ... # PYI002 + 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 + 9 | if sys.version[0] == 'P': ... # PYI002 + | ^^^^^^^^^^^^^^^^^^^^^ PYI002 +10 | if False: ... # PYI002 + | + +PYI002.pyi:10:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | + 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 + 9 | if sys.version[0] == 'P': ... # PYI002 +10 | if False: ... # PYI002 + | ^^^^^ PYI002 +11 | +12 | if version_info[0] == 2: ... + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap new file mode 100644 index 0000000000..2ce520c09b --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI003_PYI003.pyi.snap @@ -0,0 +1,173 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI003.pyi:4:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:5:4: PYI003 Unrecognized `sys.version_info` check + | +3 | if sys.version_info[0] == 2: ... +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:6:4: PYI003 Unrecognized `sys.version_info` check + | +4 | if sys.version_info[0] == True: ... # Y003 Unrecognized sys.version_info check # E712 comparison to True should be 'if cond is True:' or 'if cond:' +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:7:4: PYI003 Unrecognized `sys.version_info` check + | +5 | if sys.version_info[0.0] == 2: ... # Y003 Unrecognized sys.version_info check +6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check +7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check +9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:8:4: PYI003 Unrecognized `sys.version_info` check + | + 6 | if sys.version_info[False] == 2: ... # Y003 Unrecognized sys.version_info check + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:9:4: PYI003 Unrecognized `sys.version_info` check + | + 7 | if sys.version_info[0j] == 2: ... # Y003 Unrecognized sys.version_info check + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:10:4: PYI003 Unrecognized `sys.version_info` check + | + 8 | if sys.version_info[0] == (2, 7): ... # Y003 Unrecognized sys.version_info check + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:11:4: PYI003 Unrecognized `sys.version_info` check + | + 9 | if sys.version_info[0] == '2': ... # Y003 Unrecognized sys.version_info check +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:12:4: PYI003 Unrecognized `sys.version_info` check + | +10 | if sys.version_info[1:] >= (7, 11): ... # Y003 Unrecognized sys.version_info check +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... + | + +PYI003.pyi:13:4: PYI003 Unrecognized `sys.version_info` check + | +11 | if sys.version_info[::-1] < (11, 7): ... # Y003 Unrecognized sys.version_info check +12 | if sys.version_info[:3] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:15:4: PYI003 Unrecognized `sys.version_info` check + | +13 | if sys.version_info[:True] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +14 | if sys.version_info[:1] == (2,): ... +15 | if sys.version_info[:1] == (True,): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +16 | if sys.version_info[:1] == (2, 7): ... # Y005 Version comparison must be against a length-1 tuple +17 | if sys.version_info[:2] == (2, 7): ... + | + +PYI003.pyi:19:4: PYI003 Unrecognized `sys.version_info` check + | +17 | if sys.version_info[:2] == (2, 7): ... +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:20:4: PYI003 Unrecognized `sys.version_info` check + | +18 | if sys.version_info[:2] == (2,): ... # Y005 Version comparison must be against a length-2 tuple +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:21:4: PYI003 Unrecognized `sys.version_info` check + | +19 | if sys.version_info[:2] == "lol": ... # Y003 Unrecognized sys.version_info check +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:22:4: PYI003 Unrecognized `sys.version_info` check + | +20 | if sys.version_info[:2.0] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | + +PYI003.pyi:23:4: PYI003 Unrecognized `sys.version_info` check + | +21 | if sys.version_info[:2j] >= (3, 9): ... # Y003 Unrecognized sys.version_info check +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version + | + +PYI003.pyi:24:4: PYI003 Unrecognized `sys.version_info` check + | +22 | if sys.version_info[:, :] >= (2, 7): ... # Y003 Unrecognized sys.version_info check +23 | if sys.version_info < [3, 0]: ... # Y003 Unrecognized sys.version_info check +24 | if sys.version_info < ('3', '0'): ... # Y003 Unrecognized sys.version_info check + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI003 +25 | if sys.version_info >= (3, 4, 3): ... # Y004 Version comparison must use only major and minor version +26 | if sys.version_info == (3, 4): ... # Y006 Use only < and >= for version comparisons + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap new file mode 100644 index 0000000000..ddb37572e5 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI004_PYI004.pyi.snap @@ -0,0 +1,42 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI004.pyi:4:4: PYI004 Version comparison must use only major and minor version + | +2 | from sys import version_info +3 | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:5:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:6:4: PYI004 Version comparison must use only major and minor version + | +4 | if sys.version_info >= (3, 4, 3): ... # PYI004 +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | + +PYI004.pyi:7:4: PYI004 Version comparison must use only major and minor version + | +5 | if sys.version_info < (3, 4, 3): ... # PYI004 +6 | if sys.version_info == (3, 4, 3): ... # PYI004 +7 | if sys.version_info != (3, 4, 3): ... # PYI004 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI004 +8 | +9 | if sys.version_info[0] == 2: ... + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap new file mode 100644 index 0000000000..4b11f74662 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple. + | +2 | from sys import platform, version_info +3 | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | + +PYI005.pyi:5:4: PYI005 Version comparison must be against a length-2 tuple. + | +4 | if sys.version_info[:1] == (2, 7): ... # Y005 +5 | if sys.version_info[:2] == (2,): ... # Y005 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI005 + | + + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap index 3c0293215f..8dbd74304f 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI006_PYI006.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:8:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 6 | if sys.version_info >= (3, 9): ... # OK 7 | @@ -11,7 +11,7 @@ PYI006.pyi:8:4: PYI006 Use `<` or `>=` for version info comparisons 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:10:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 8 | if sys.version_info == (3, 9): ... # OK 9 | @@ -21,7 +21,7 @@ PYI006.pyi:10:4: PYI006 Use `<` or `>=` for version info comparisons 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:12:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 10 | if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 11 | @@ -31,7 +31,7 @@ PYI006.pyi:12:4: PYI006 Use `<` or `>=` for version info comparisons 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:14:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 12 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 13 | @@ -41,7 +41,7 @@ PYI006.pyi:14:4: PYI006 Use `<` or `>=` for version info comparisons 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:16:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 14 | if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 15 | @@ -51,7 +51,7 @@ PYI006.pyi:16:4: PYI006 Use `<` or `>=` for version info comparisons 18 | if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons | -PYI006.pyi:18:4: PYI006 Use `<` or `>=` for version info comparisons +PYI006.pyi:18:4: PYI006 Use `<` or `>=` for `sys.version_info` comparisons | 16 | if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons 17 | diff --git a/ruff.schema.json b/ruff.schema.json index 8875d1423c..8e7d37f7e5 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2308,6 +2308,10 @@ "PYI0", "PYI00", "PYI001", + "PYI002", + "PYI003", + "PYI004", + "PYI005", "PYI006", "PYI007", "PYI008", From 93b2bd7184d15214c7fa9d473e9621ad29d97cfc Mon Sep 17 00:00:00 2001 From: qdegraaf <34540841+qdegraaf@users.noreply.github.com> Date: Mon, 3 Jul 2023 06:03:09 +0200 Subject: [PATCH 08/27] [`perflint`] Add `PERF401` and `PERF402` rules (#5298) ## Summary Adds `PERF401` and `PERF402` mirroring `W8401` and `W8402` from https://github.com/tonybaloney/perflint Implementation is not super smart but should be at parity with upstream implementation judging by: https://github.com/tonybaloney/perflint/blob/c07391c17671c3c9d5a7fd69120d1f570e268d58/perflint/comprehension_checker.py#L42-L73 It essentially checks: - If the body of a for-loop is just one statement - If that statement is an `if` and the if-statement contains a call to `append()` we flag `PERF401` and suggest a list comprehension - If that statement is a plain call to `append()` or `insert()` we flag `PERF402` and suggest `list()` or `list.copy()` I've set the violation to only flag the first append call in a long `if-else` statement for `PERF401`. Happy to change this to some other location or make it multiple violations if that makes more sense. ## Test Plan Fixtures were added with the relevant scenarios for both rules ## Issue Links Refers: https://github.com/astral-sh/ruff/issues/4789 --- .../test/fixtures/perflint/PERF401.py | 18 +++++ .../test/fixtures/perflint/PERF402.py | 12 +++ crates/ruff/src/checkers/ast/mod.rs | 6 ++ crates/ruff/src/codes.rs | 2 + crates/ruff/src/rules/perflint/mod.rs | 2 + .../perflint/rules/incorrect_dict_iterator.rs | 4 + .../rules/manual_list_comprehension.rs | 76 +++++++++++++++++++ .../rules/perflint/rules/manual_list_copy.rs | 69 +++++++++++++++++ crates/ruff/src/rules/perflint/rules/mod.rs | 4 + .../perflint/rules/unnecessary_list_cast.rs | 4 + ...__perflint__tests__PERF401_PERF401.py.snap | 22 ++++++ ...__perflint__tests__PERF402_PERF402.py.snap | 20 +++++ ruff.schema.json | 4 + scripts/check_docs_formatted.py | 12 +-- scripts/update_ambiguous_characters.py | 4 +- 15 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/perflint/PERF401.py create mode 100644 crates/ruff/resources/test/fixtures/perflint/PERF402.py create mode 100644 crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs create mode 100644 crates/ruff/src/rules/perflint/rules/manual_list_copy.rs create mode 100644 crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap create mode 100644 crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py new file mode 100644 index 0000000000..ac19d19876 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -0,0 +1,18 @@ +def foo(): + items = [1, 2, 3, 4] + result = [] + for i in items: + if i % 2: + result.append(i) # PERF401 + + +def foo(): + items = [1,2,3,4] + result = [] + for i in items: + if i % 2: + result.append(i) # PERF401 + elif i % 2: + result.append(i) # PERF401 + else: + result.append(i) # PERF401 diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF402.py b/crates/ruff/resources/test/fixtures/perflint/PERF402.py new file mode 100644 index 0000000000..0d6842dce7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/perflint/PERF402.py @@ -0,0 +1,12 @@ +def foo(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i) # PERF402 + + +def foo(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.insert(0, i) # PERF402 diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 0c9f892cce..d20c1271d6 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1501,6 +1501,12 @@ where if self.enabled(Rule::IncorrectDictIterator) { perflint::rules::incorrect_dict_iterator(self, target, iter); } + if self.enabled(Rule::ManualListComprehension) { + perflint::rules::manual_list_comprehension(self, body); + } + if self.enabled(Rule::ManualListCopy) { + perflint::rules::manual_list_copy(self, body); + } if self.enabled(Rule::UnnecessaryListCast) { perflint::rules::unnecessary_list_cast(self, iter); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 06adeba95b..79a7982dc9 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -793,6 +793,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast), (Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator), (Perflint, "203") => (RuleGroup::Unspecified, rules::perflint::rules::TryExceptInLoop), + (Perflint, "401") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListComprehension), + (Perflint, "402") => (RuleGroup::Unspecified, rules::perflint::rules::ManualListCopy), // flake8-fixme (Flake8Fixme, "001") => (RuleGroup::Unspecified, rules::flake8_fixme::rules::LineContainsFixme), diff --git a/crates/ruff/src/rules/perflint/mod.rs b/crates/ruff/src/rules/perflint/mod.rs index 33c9691206..291bfcd207 100644 --- a/crates/ruff/src/rules/perflint/mod.rs +++ b/crates/ruff/src/rules/perflint/mod.rs @@ -16,6 +16,8 @@ mod tests { #[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))] #[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))] #[test_case(Rule::TryExceptInLoop, Path::new("PERF203.py"))] + #[test_case(Rule::ManualListComprehension, Path::new("PERF401.py"))] + #[test_case(Rule::ManualListCopy, Path::new("PERF402.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs index 17e2d2f93b..af2677f2f1 100644 --- a/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs +++ b/crates/ruff/src/rules/perflint/rules/incorrect_dict_iterator.rs @@ -23,6 +23,10 @@ use crate::registry::AsRule; /// avoid allocating tuples for every item in the dictionary. They also /// communicate the intent of the code more clearly. /// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// /// ## Example /// ```python /// some_dict = {"a": 1, "b": 2} diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs new file mode 100644 index 0000000000..d7143bb080 --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -0,0 +1,76 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a list comprehension. +/// +/// ## Why is this bad? +/// When creating a filtered list from an existing list using a for-loop, +/// prefer a list comprehension. List comprehensions are more readable and +/// more performant. +/// +/// Using the below as an example, the list comprehension is ~10% faster on +/// Python 3.11, and ~25% faster on Python 3.10. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// if i % 2: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = [x for x in original if x % 2] +/// ``` +#[violation] +pub struct ManualListComprehension; + +impl Violation for ManualListComprehension { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use a list comprehension to create a new filtered list") + } +} + +/// PERF401 +pub(crate) fn manual_list_comprehension(checker: &mut Checker, body: &[Stmt]) { + let [stmt] = body else { + return; + }; + + let Stmt::If(ast::StmtIf { body, .. }) = stmt else { + return; + }; + + for stmt in body { + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + continue; + }; + + let Expr::Call(ast::ExprCall { func, range, .. }) = value.as_ref() else { + continue; + }; + + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + continue; + }; + + if attr.as_str() == "append" { + checker + .diagnostics + .push(Diagnostic::new(ManualListComprehension, *range)); + } + } +} diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs new file mode 100644 index 0000000000..dd7545129d --- /dev/null +++ b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs @@ -0,0 +1,69 @@ +use rustpython_parser::ast::{self, Expr, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `for` loops that can be replaced by a making a copy of a list. +/// +/// ## Why is this bad? +/// When creating a copy of an existing list using a for-loop, prefer +/// `list` or `list.copy` instead. Making a direct copy is more readable and +/// more performant. +/// +/// Using the below as an example, the `list`-based copy is ~2x faster on +/// Python 3.11. +/// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// +/// ## Example +/// ```python +/// original = list(range(10000)) +/// filtered = [] +/// for i in original: +/// filtered.append(i) +/// ``` +/// +/// Use instead: +/// ```python +/// original = list(range(10000)) +/// filtered = list(original) +/// ``` +#[violation] +pub struct ManualListCopy; + +impl Violation for ManualListCopy { + #[derive_message_formats] + fn message(&self) -> String { + format!("Use `list` or `list.copy` to create a copy of a list") + } +} + +/// PERF402 +pub(crate) fn manual_list_copy(checker: &mut Checker, body: &[Stmt]) { + let [stmt] = body else { + return; + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return; + }; + + let Expr::Call(ast::ExprCall { func, range, .. }) = value.as_ref() else { + return; + }; + + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + return; + }; + + if matches!(attr.as_str(), "append" | "insert") { + checker + .diagnostics + .push(Diagnostic::new(ManualListCopy, *range)); + } +} diff --git a/crates/ruff/src/rules/perflint/rules/mod.rs b/crates/ruff/src/rules/perflint/rules/mod.rs index 4af80c1432..690b0fc1fe 100644 --- a/crates/ruff/src/rules/perflint/rules/mod.rs +++ b/crates/ruff/src/rules/perflint/rules/mod.rs @@ -1,7 +1,11 @@ pub(crate) use incorrect_dict_iterator::*; +pub(crate) use manual_list_comprehension::*; +pub(crate) use manual_list_copy::*; pub(crate) use try_except_in_loop::*; pub(crate) use unnecessary_list_cast::*; mod incorrect_dict_iterator; +mod manual_list_comprehension; +mod manual_list_copy; mod try_except_in_loop; mod unnecessary_list_cast; diff --git a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs index 900b22cc52..670256d10a 100644 --- a/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs +++ b/crates/ruff/src/rules/perflint/rules/unnecessary_list_cast.rs @@ -19,6 +19,10 @@ use crate::registry::AsRule; /// Removing the `list()` call will not change the behavior of the code, but /// may improve performance. /// +/// Note that, as with all `perflint` rules, this is only intended as a +/// micro-optimization, and will have a negligible impact on performance in +/// most cases. +/// /// ## Example /// ```python /// items = (1, 2, 3) diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap new file mode 100644 index 0000000000..e59eae4adc --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap @@ -0,0 +1,22 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF401.py:6:13: PERF401 Use a list comprehension to create a new filtered list + | +4 | for i in items: +5 | if i % 2: +6 | result.append(i) # PERF401 + | ^^^^^^^^^^^^^^^^ PERF401 + | + +PERF401.py:14:13: PERF401 Use a list comprehension to create a new filtered list + | +12 | for i in items: +13 | if i % 2: +14 | result.append(i) # PERF401 + | ^^^^^^^^^^^^^^^^ PERF401 +15 | elif i % 2: +16 | result.append(i) # PERF401 + | + + diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap new file mode 100644 index 0000000000..55cd69db8b --- /dev/null +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/perflint/mod.rs +--- +PERF402.py:5:9: PERF402 Use `list` or `list.copy` to create a copy of a list + | +3 | result = [] +4 | for i in items: +5 | result.append(i) # PERF402 + | ^^^^^^^^^^^^^^^^ PERF402 + | + +PERF402.py:12:9: PERF402 Use `list` or `list.copy` to create a copy of a list + | +10 | result = [] +11 | for i in items: +12 | result.insert(0, i) # PERF402 + | ^^^^^^^^^^^^^^^^^^^ PERF402 + | + + diff --git a/ruff.schema.json b/ruff.schema.json index 8e7d37f7e5..785fac6520 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2093,6 +2093,10 @@ "PERF2", "PERF20", "PERF203", + "PERF4", + "PERF40", + "PERF401", + "PERF402", "PGH", "PGH0", "PGH00", diff --git a/scripts/check_docs_formatted.py b/scripts/check_docs_formatted.py index be2ef93a46..f10e38cb0e 100755 --- a/scripts/check_docs_formatted.py +++ b/scripts/check_docs_formatted.py @@ -186,10 +186,7 @@ def main(argv: Sequence[str] | None = None) -> int: generate_docs() # Get static docs - static_docs = [] - for file in os.listdir("docs"): - if file.endswith(".md"): - static_docs.append(Path("docs") / file) + static_docs = [Path("docs") / f for f in os.listdir("docs") if f.endswith(".md")] # Check rules generated if not Path("docs/rules").exists(): @@ -197,10 +194,9 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 # Get generated rules - generated_docs = [] - for file in os.listdir("docs/rules"): - if file.endswith(".md"): - generated_docs.append(Path("docs/rules") / file) + generated_docs = [ + Path("docs/rules") / f for f in os.listdir("docs/rules") if f.endswith(".md") + ] if len(generated_docs) == 0: print("Please generate rules first.") diff --git a/scripts/update_ambiguous_characters.py b/scripts/update_ambiguous_characters.py index 27a06fd039..cf165af585 100644 --- a/scripts/update_ambiguous_characters.py +++ b/scripts/update_ambiguous_characters.py @@ -45,9 +45,7 @@ def format_confusables_rs(raw_data: dict[str, list[int]]) -> str: for i in range(0, len(items), 2): flattened_items.add((items[i], items[i + 1])) - tuples = [] - for left, right in sorted(flattened_items): - tuples.append(f" {left}u32 => {right},\n") + tuples = [f" {left}u32 => {right},\n" for left, right in sorted(flattened_items)] print(f"{len(tuples)} confusable tuples.") From 94ac2c4e1bbb583acc0d03e82c236b1c9398d324 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 00:39:22 -0400 Subject: [PATCH 09/27] Reorganize some `flake8-pyi` rules (#5472) --- .../test/fixtures/flake8_pyi/PYI002.py | 19 +- .../test/fixtures/flake8_pyi/PYI002.pyi | 19 +- crates/ruff/src/checkers/ast/mod.rs | 62 +++--- .../rules/bad_version_info_comparison.rs | 27 +-- .../rules/complex_if_statement_in_stub.rs | 80 ++++++++ crates/ruff/src/rules/flake8_pyi/rules/mod.rs | 6 +- .../flake8_pyi/rules/unrecognized_platform.rs | 62 +++--- ...n_info.rs => unrecognized_version_info.rs} | 184 ++++++------------ ..._flake8_pyi__tests__PYI002_PYI002.pyi.snap | 78 +++----- ..._flake8_pyi__tests__PYI005_PYI005.pyi.snap | 4 +- 10 files changed, 257 insertions(+), 284 deletions(-) create mode 100644 crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs rename crates/ruff/src/rules/flake8_pyi/rules/{version_info.rs => unrecognized_version_info.rs} (63%) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py index 857b029cdc..50cf7c884f 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.py @@ -1,17 +1,6 @@ import sys -from sys import platform, version_info -if sys.version == 'Python 2.7.10': ... # PYI002 -if 'linux' == sys.platform: ... # PYI002 -if hasattr(sys, 'maxint'): ... # PYI002 -if sys.maxsize == 42: ... # PYI002 -if (2, 7) < sys.version_info < (3, 5): ... # PYI002 -if sys.version[0] == 'P': ... # PYI002 -if False: ... # PYI002 - -if version_info[0] == 2: ... -if sys.version_info < (3, 5): ... -if version_info >= (3, 5): ... -if sys.version_info[:2] == (2, 7): ... -if sys.version_info[:1] == (2,): ... -if platform == 'linux': ... +if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi index 857b029cdc..50cf7c884f 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI002.pyi @@ -1,17 +1,6 @@ import sys -from sys import platform, version_info -if sys.version == 'Python 2.7.10': ... # PYI002 -if 'linux' == sys.platform: ... # PYI002 -if hasattr(sys, 'maxint'): ... # PYI002 -if sys.maxsize == 42: ... # PYI002 -if (2, 7) < sys.version_info < (3, 5): ... # PYI002 -if sys.version[0] == 'P': ... # PYI002 -if False: ... # PYI002 - -if version_info[0] == 2: ... -if sys.version_info < (3, 5): ... -if version_info >= (3, 5): ... -if sys.version_info[:2] == (2, 7): ... -if sys.version_info[:1] == (2,): ... -if platform == 'linux': ... +if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index d20c1271d6..8d85f2968c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1373,12 +1373,47 @@ where } if self.is_stub { if self.any_enabled(&[ - Rule::ComplexIfStatementInStub, Rule::UnrecognizedVersionInfoCheck, Rule::PatchVersionComparison, Rule::WrongTupleLengthVersionComparison, ]) { - flake8_pyi::rules::version_info(self, test); + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_version_info(self, value); + } + } else { + flake8_pyi::rules::unrecognized_version_info(self, test); + } + } + if self.any_enabled(&[ + Rule::UnrecognizedPlatformCheck, + Rule::UnrecognizedPlatformName, + ]) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::unrecognized_platform(self, value); + } + } else { + flake8_pyi::rules::unrecognized_platform(self, test); + } + } + if self.enabled(Rule::BadVersionInfoComparison) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::bad_version_info_comparison(self, value); + } + } else { + flake8_pyi::rules::bad_version_info_comparison(self, test); + } + } + if self.enabled(Rule::ComplexIfStatementInStub) { + if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() { + for value in values { + flake8_pyi::rules::complex_if_statement_in_stub(self, value); + } + } else { + flake8_pyi::rules::complex_if_statement_in_stub(self, test); + } } } } @@ -3223,29 +3258,6 @@ where if self.enabled(Rule::YodaConditions) { flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators); } - if self.is_stub { - if self.any_enabled(&[ - Rule::UnrecognizedPlatformCheck, - Rule::UnrecognizedPlatformName, - ]) { - flake8_pyi::rules::unrecognized_platform( - self, - expr, - left, - ops, - comparators, - ); - } - if self.enabled(Rule::BadVersionInfoComparison) { - flake8_pyi::rules::bad_version_info_comparison( - self, - expr, - left, - ops, - comparators, - ); - } - } } Expr::Constant(ast::ExprConstant { value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. }, diff --git a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs index 18a8d4e1b1..332ddfc5f7 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/bad_version_info_comparison.rs @@ -1,4 +1,4 @@ -use rustpython_parser::ast::{CmpOp, Expr, Ranged}; +use rustpython_parser::ast::{self, CmpOp, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -57,14 +57,18 @@ impl Violation for BadVersionInfoComparison { } /// PYI006 -pub(crate) fn bad_version_info_comparison( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[CmpOp], - comparators: &[Expr], -) { - let ([op], [_right]) = (ops, comparators) else { +pub(crate) fn bad_version_info_comparison(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [_right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; @@ -82,6 +86,7 @@ pub(crate) fn bad_version_info_comparison( return; } - let diagnostic = Diagnostic::new(BadVersionInfoComparison, expr.range()); - checker.diagnostics.push(diagnostic); + checker + .diagnostics + .push(Diagnostic::new(BadVersionInfoComparison, test.range())); } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs new file mode 100644 index 0000000000..a8287ff671 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/complex_if_statement_in_stub.rs @@ -0,0 +1,80 @@ +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `if` statements with complex conditionals in stubs. +/// +/// ## Why is this bad? +/// Stub files support simple conditionals to test for differences in Python +/// versions and platforms. However, type checkers only understand a limited +/// subset of these conditionals; complex conditionals may result in false +/// positives or false negatives. +/// +/// ## Example +/// ```python +/// import sys +/// +/// if (2, 7) < sys.version_info < (3, 5): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// import sys +/// +/// if sys.version_info < (3, 5): +/// ... +/// ``` +#[violation] +pub struct ComplexIfStatementInStub; + +impl Violation for ComplexIfStatementInStub { + #[derive_message_formats] + fn message(&self) -> String { + format!( + "`if`` test must be a simple comparison against `sys.platform` or `sys.version_info`" + ) + } +} + +/// PYI002 +pub(crate) fn complex_if_statement_in_stub(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, comparators, .. + }) = test + else { + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); + return; + }; + + if comparators.len() != 1 { + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); + return; + } + + if left.is_subscript_expr() { + return; + } + + if checker + .semantic() + .resolve_call_path(left) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "version_info" | "platform"]) + }) + { + return; + } + + checker + .diagnostics + .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); +} diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index ed96285dcd..612055db37 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -1,6 +1,7 @@ pub(crate) use any_eq_ne_annotation::*; pub(crate) use bad_version_info_comparison::*; pub(crate) use collections_named_tuple::*; +pub(crate) use complex_if_statement_in_stub::*; pub(crate) use docstring_in_stubs::*; pub(crate) use duplicate_union_member::*; pub(crate) use ellipsis_in_non_empty_class_body::*; @@ -22,11 +23,12 @@ pub(crate) use type_alias_naming::*; pub(crate) use type_comment_in_stub::*; pub(crate) use unaliased_collections_abc_set_import::*; pub(crate) use unrecognized_platform::*; -pub(crate) use version_info::*; +pub(crate) use unrecognized_version_info::*; mod any_eq_ne_annotation; mod bad_version_info_comparison; mod collections_named_tuple; +mod complex_if_statement_in_stub; mod docstring_in_stubs; mod duplicate_union_member; mod ellipsis_in_non_empty_class_body; @@ -48,4 +50,4 @@ mod type_alias_naming; mod type_comment_in_stub; mod unaliased_collections_abc_set_import; mod unrecognized_platform; -mod version_info; +mod unrecognized_version_info; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs index af876baa0c..418c0c55bf 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_platform.rs @@ -89,19 +89,21 @@ impl Violation for UnrecognizedPlatformName { } /// PYI007, PYI008 -pub(crate) fn unrecognized_platform( - checker: &mut Checker, - expr: &Expr, - left: &Expr, - ops: &[CmpOp], - comparators: &[Expr], -) { - let ([op], [right]) = (ops, comparators) else { +pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { + return; + }; + + let ([op], [right]) = (ops.as_slice(), comparators.as_slice()) else { return; }; - let diagnostic_unrecognized_platform_check = - Diagnostic::new(UnrecognizedPlatformCheck, expr.range()); if !checker .semantic() .resolve_call_path(left) @@ -113,23 +115,24 @@ pub(crate) fn unrecognized_platform( } // "in" might also make sense but we don't currently have one. - if !matches!(op, CmpOp::Eq | CmpOp::NotEq) && checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); + if !matches!(op, CmpOp::Eq | CmpOp::NotEq) { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); + } return; } - match right { - Expr::Constant(ast::ExprConstant { - value: Constant::Str(value), - .. - }) => { - // Other values are possible but we don't need them right now. - // This protects against typos. - if !["linux", "win32", "cygwin", "darwin"].contains(&value.as_str()) - && checker.enabled(Rule::UnrecognizedPlatformName) - { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(value), + .. + }) = right + { + // Other values are possible but we don't need them right now. + // This protects against typos. + if checker.enabled(Rule::UnrecognizedPlatformName) { + if !matches!(value.as_str(), "linux" | "win32" | "cygwin" | "darwin") { checker.diagnostics.push(Diagnostic::new( UnrecognizedPlatformName { platform: value.clone(), @@ -138,12 +141,11 @@ pub(crate) fn unrecognized_platform( )); } } - _ => { - if checker.enabled(Rule::UnrecognizedPlatformCheck) { - checker - .diagnostics - .push(diagnostic_unrecognized_platform_check); - } + } else { + if checker.enabled(Rule::UnrecognizedPlatformCheck) { + checker + .diagnostics + .push(Diagnostic::new(UnrecognizedPlatformCheck, test.range())); } } } diff --git a/crates/ruff/src/rules/flake8_pyi/rules/version_info.rs b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs similarity index 63% rename from crates/ruff/src/rules/flake8_pyi/rules/version_info.rs rename to crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs index 4e2e2d21de..dfb288154c 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/version_info.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/unrecognized_version_info.rs @@ -1,50 +1,14 @@ use num_bigint::BigInt; use num_traits::{One, Zero}; use rustpython_parser::ast::{self, CmpOp, Constant, Expr, Ranged}; -use smallvec::SmallVec; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::map_subscript; use crate::checkers::ast::Checker; use crate::registry::Rule; -/// ## What it does -/// Checks for `if` statements with complex conditionals in stubs. -/// -/// ## Why is this bad? -/// Stub files support simple conditionals to test for differences in Python -/// versions and platforms. However, type checkers only understand a limited -/// subset of these conditionals; complex conditionals may result in false -/// positives or false negatives. -/// -/// ## Example -/// ```python -/// import sys -/// -/// if (2, 7) < sys.version_info < (3, 5): -/// ... -/// ``` -/// -/// Use instead: -/// ```python -/// import sys -/// -/// if sys.version_info < (3, 5): -/// ... -/// ``` -#[violation] -pub struct ComplexIfStatementInStub; - -impl Violation for ComplexIfStatementInStub { - #[derive_message_formats] - fn message(&self) -> String { - format!( - "`if`` test must be a simple comparison against `sys.platform` or `sys.version_info`" - ) - } -} - /// ## What it does /// Checks for problematic `sys.version_info`-related conditions in stubs. /// @@ -150,104 +114,57 @@ pub struct WrongTupleLengthVersionComparison { impl Violation for WrongTupleLengthVersionComparison { #[derive_message_formats] fn message(&self) -> String { - format!( - "Version comparison must be against a length-{} tuple.", - self.expected_length - ) + let WrongTupleLengthVersionComparison { expected_length } = self; + format!("Version comparison must be against a length-{expected_length} tuple") } } -#[derive(Copy, Clone, Eq, PartialEq)] -enum ExpectedComparator { - MajorDigit, - MajorTuple, - MajorMinorTuple, - AnyTuple, -} - -/// PYI002, PYI003, PYI004, PYI005 -pub(crate) fn version_info(checker: &mut Checker, test: &Expr) { - if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test { - for value in values { - version_info(checker, value); - } - return; - } - - let Some((left, op, comparator, is_platform)) = compare_expr_components(checker, test) else { - if checker.enabled(Rule::ComplexIfStatementInStub) { - checker - .diagnostics - .push(Diagnostic::new(ComplexIfStatementInStub, test.range())); - } +/// PYI003, PYI004, PYI005 +pub(crate) fn unrecognized_version_info(checker: &mut Checker, test: &Expr) { + let Expr::Compare(ast::ExprCompare { + left, + ops, + comparators, + .. + }) = test + else { return; }; - // Already covered by PYI007. - if is_platform { + let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) else { + return; + }; + + if !checker + .semantic() + .resolve_call_path(map_subscript(left)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["sys", "version_info"]) + }) + { return; } - let Ok(expected_comparator) = ExpectedComparator::try_from(left) else { + if let Some(expected) = ExpectedComparator::try_from(left) { + version_check(checker, expected, test, *op, comparator); + } else { if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { checker .diagnostics .push(Diagnostic::new(UnrecognizedVersionInfoCheck, test.range())); } - return; - }; - - check_version_check(checker, expected_comparator, test, op, comparator); + } } -/// Extracts relevant components of the if test. -fn compare_expr_components<'a>( - checker: &Checker, - test: &'a Expr, -) -> Option<(&'a Expr, CmpOp, &'a Expr, bool)> { - test.as_compare_expr().and_then(|cmp| { - let ast::ExprCompare { - left, - ops, - comparators, - .. - } = cmp; - - if comparators.len() != 1 { - return None; - } - - let name_expr = if let Expr::Subscript(ast::ExprSubscript { value, .. }) = left.as_ref() { - value - } else { - left - }; - - // The only valid comparisons are against sys.platform and sys.version_info. - let is_platform = match checker - .semantic() - .resolve_call_path(name_expr) - .as_ref() - .map(SmallVec::as_slice) - { - Some(["sys", "platform"]) => true, - Some(["sys", "version_info"]) => false, - _ => return None, - }; - - Some((left.as_ref(), ops[0], &comparators[0], is_platform)) - }) -} - -fn check_version_check( +fn version_check( checker: &mut Checker, - expected_comparator: ExpectedComparator, + expected: ExpectedComparator, test: &Expr, op: CmpOp, comparator: &Expr, ) { // Single digit comparison, e.g., `sys.version_info[0] == 2`. - if expected_comparator == ExpectedComparator::MajorDigit { + if expected == ExpectedComparator::MajorDigit { if !is_int_constant(comparator) { if checker.enabled(Rule::UnrecognizedVersionInfoCheck) { checker @@ -288,7 +205,7 @@ fn check_version_check( if checker.enabled(Rule::WrongTupleLengthVersionComparison) { if op == CmpOp::Eq || op == CmpOp::NotEq { - let expected_length = match expected_comparator { + let expected_length = match expected { ExpectedComparator::MajorTuple => 1, ExpectedComparator::MajorMinorTuple => 2, _ => return, @@ -304,32 +221,40 @@ fn check_version_check( } } -impl TryFrom<&Expr> for ExpectedComparator { - type Error = (); +#[derive(Copy, Clone, Eq, PartialEq)] +enum ExpectedComparator { + MajorDigit, + MajorTuple, + MajorMinorTuple, + AnyTuple, +} - fn try_from(value: &Expr) -> Result { - let Expr::Subscript(ast::ExprSubscript { slice, .. }) = value else { - return Ok(ExpectedComparator::AnyTuple) +impl ExpectedComparator { + /// Returns the expected comparator for the given expression, if any. + fn try_from(expr: &Expr) -> Option { + let Expr::Subscript(ast::ExprSubscript { slice, .. }) = expr else { + return Some(ExpectedComparator::AnyTuple); }; - // Only allow simple slices of the form [:n] or explicit indexing into the first element + // Only allow: (1) simple slices of the form `[:n]`, or (2) explicit indexing into the first + // element (major version) of the tuple. match slice.as_ref() { Expr::Slice(ast::ExprSlice { lower: None, - upper: Some(n), + upper: Some(upper), step: None, .. }) => { if let Expr::Constant(ast::ExprConstant { - value: Constant::Int(n), + value: Constant::Int(upper), .. - }) = n.as_ref() + }) = upper.as_ref() { - if *n == BigInt::one() { - return Ok(ExpectedComparator::MajorTuple); + if *upper == BigInt::one() { + return Some(ExpectedComparator::MajorTuple); } - if *n == BigInt::from(2) { - return Ok(ExpectedComparator::MajorMinorTuple); + if *upper == BigInt::from(2) { + return Some(ExpectedComparator::MajorMinorTuple); } } } @@ -337,15 +262,16 @@ impl TryFrom<&Expr> for ExpectedComparator { value: Constant::Int(n), .. }) if n.is_zero() => { - return Ok(ExpectedComparator::MajorDigit); + return Some(ExpectedComparator::MajorDigit); } _ => (), } - Err(()) + None } } +/// Returns `true` if the given expression is an integer constant. fn is_int_constant(expr: &Expr) -> bool { matches!( expr, diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap index bfac1f9b31..103bef4bac 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI002_PYI002.pyi.snap @@ -1,72 +1,40 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- +PYI002.pyi:3:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` + | +1 | import sys +2 | +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | + PYI002.pyi:4:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` | -2 | from sys import platform, version_info -3 | -4 | if sys.version == 'Python 2.7.10': ... # PYI002 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 -5 | if 'linux' == sys.platform: ... # PYI002 -6 | if hasattr(sys, 'maxint'): ... # PYI002 +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^^ PYI002 +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info | PYI002.pyi:5:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` | -4 | if sys.version == 'Python 2.7.10': ... # PYI002 -5 | if 'linux' == sys.platform: ... # PYI002 - | ^^^^^^^^^^^^^^^^^^^^^^^ PYI002 -6 | if hasattr(sys, 'maxint'): ... # PYI002 -7 | if sys.maxsize == 42: ... # PYI002 +3 | if sys.version == 'Python 2.7.10': ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info + | ^^^^^^^^^^^^^^^^^^^^^^ PYI002 +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info | PYI002.pyi:6:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` | -4 | if sys.version == 'Python 2.7.10': ... # PYI002 -5 | if 'linux' == sys.platform: ... # PYI002 -6 | if hasattr(sys, 'maxint'): ... # PYI002 - | ^^^^^^^^^^^^^^^^^^^^^^ PYI002 -7 | if sys.maxsize == 42: ... # PYI002 -8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 - | - -PYI002.pyi:7:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` - | -5 | if 'linux' == sys.platform: ... # PYI002 -6 | if hasattr(sys, 'maxint'): ... # PYI002 -7 | if sys.maxsize == 42: ... # PYI002 +4 | if 'linux' == sys.platform: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +5 | if hasattr(sys, 'maxint'): ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info +6 | if sys.maxsize == 42: ... # Y002 If test must be a simple comparison against sys.platform or sys.version_info | ^^^^^^^^^^^^^^^^^ PYI002 -8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 -9 | if sys.version[0] == 'P': ... # PYI002 | -PYI002.pyi:8:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` - | - 6 | if hasattr(sys, 'maxint'): ... # PYI002 - 7 | if sys.maxsize == 42: ... # PYI002 - 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI002 - 9 | if sys.version[0] == 'P': ... # PYI002 -10 | if False: ... # PYI002 - | - -PYI002.pyi:9:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` - | - 7 | if sys.maxsize == 42: ... # PYI002 - 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 - 9 | if sys.version[0] == 'P': ... # PYI002 - | ^^^^^^^^^^^^^^^^^^^^^ PYI002 -10 | if False: ... # PYI002 - | - -PYI002.pyi:10:4: PYI002 `if`` test must be a simple comparison against `sys.platform` or `sys.version_info` - | - 8 | if (2, 7) < sys.version_info < (3, 5): ... # PYI002 - 9 | if sys.version[0] == 'P': ... # PYI002 -10 | if False: ... # PYI002 - | ^^^^^ PYI002 -11 | -12 | if version_info[0] == 2: ... - | - diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap index 4b11f74662..1641bce44c 100644 --- a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI005_PYI005.pyi.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_pyi/mod.rs --- -PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple. +PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple | 2 | from sys import platform, version_info 3 | @@ -10,7 +10,7 @@ PYI005.pyi:4:4: PYI005 Version comparison must be against a length-1 tuple. 5 | if sys.version_info[:2] == (2,): ... # Y005 | -PYI005.pyi:5:4: PYI005 Version comparison must be against a length-2 tuple. +PYI005.pyi:5:4: PYI005 Version comparison must be against a length-2 tuple | 4 | if sys.version_info[:1] == (2, 7): ... # Y005 5 | if sys.version_info[:2] == (2,): ... # Y005 From ca6ff72404604dd712381c6d3cca4cf8199320ed Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 3 Jul 2023 09:11:14 +0200 Subject: [PATCH 10/27] Change generator formatting dummy to include NOT_YET_IMPLEMENTED (#5464) ## Summary Change generator formatting dummy to include `NOT_YET_IMPLEMENTED`. This makes it easier to correctly identify them as dummies ## Test Plan This is a dummy change --- .../src/expression/expr_generator_exp.rs | 7 ++- .../src/expression/expr_list_comp.rs | 7 ++- ...mpatibility@conditional_expression.py.snap | 16 +++--- ...y@py_310__pattern_matching_generic.py.snap | 18 ++++--- ...ompatibility@py_310__pep_572_py310.py.snap | 32 ++++++++---- ...lack_compatibility@py_37__python37.py.snap | 20 ++++---- ...black_compatibility@py_38__pep_572.py.snap | 8 +-- ...patibility@simple_cases__comments2.py.snap | 12 ++--- ...patibility@simple_cases__comments3.py.snap | 4 +- ...atibility@simple_cases__expression.py.snap | 50 ++++++++++--------- ...ity@simple_cases__power_op_spacing.py.snap | 16 +++--- ...compatibility@simple_cases__slices.py.snap | 26 +++++++--- .../format@expression__binary.py.snap | 10 +++- .../snapshots/format@statement__with.py.snap | 2 +- 14 files changed, 138 insertions(+), 90 deletions(-) diff --git a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs index 54762794e3..8cf7e8d38c 100644 --- a/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_generator_exp.rs @@ -11,7 +11,12 @@ pub struct FormatExprGeneratorExp; impl FormatNodeRule for FormatExprGeneratorExp { fn fmt_fields(&self, _item: &ExprGeneratorExp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("(i for i in [])")]) + write!( + f, + [not_yet_implemented_custom_text( + "(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])" + )] + ) } } diff --git a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs index 3ab6a61f06..5bc6a3017a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_list_comp.rs +++ b/crates/ruff_python_formatter/src/expression/expr_list_comp.rs @@ -11,7 +11,12 @@ pub struct FormatExprListComp; impl FormatNodeRule for FormatExprListComp { fn fmt_fields(&self, _item: &ExprListComp, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented_custom_text("[i for i in []]")]) + write!( + f, + [not_yet_implemented_custom_text( + "[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []]" + )] + ) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap index 5ab4cf643f..cc7b50679b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@conditional_expression.py.snap @@ -79,7 +79,7 @@ def something(): ```diff --- Black +++ Ruff -@@ -1,90 +1,48 @@ +@@ -1,90 +1,50 @@ long_kwargs_single_line = my_function( foo="test, this is a sample value", - bar=( @@ -157,21 +157,21 @@ def something(): - ) - for some_boolean_variable in some_iterable -) -+generator_expression = (i for i in []) ++generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def limit_offset_sql(self, low_mark, high_mark): """Return LIMIT/OFFSET SQL clause.""" limit, offset = self._get_limit_offset_params(low_mark, high_mark) -- return " ".join( + return " ".join( - sql - for sql in ( - "LIMIT %d" % limit if limit else None, - ("OFFSET %d" % offset) if offset else None, - ) - if sql -- ) -+ return " ".join((i for i in [])) ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) def something(): @@ -221,13 +221,15 @@ def weird_default_argument( nested = NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false -generator_expression = (i for i in []) +generator_expression = (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def limit_offset_sql(self, low_mark, high_mark): """Return LIMIT/OFFSET SQL clause.""" limit, offset = self._get_limit_offset_params(low_mark, high_mark) - return " ".join((i for i in [])) + return " ".join( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ) def something(): diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap index e1aef4ad1b..0f93e60a69 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_generic.py.snap @@ -134,7 +134,7 @@ with match() as match: def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: -@@ -23,13 +23,9 @@ +@@ -23,13 +23,11 @@ pygram.python_grammar, ] @@ -146,11 +146,13 @@ with match() as match: + NOT_YET_IMPLEMENTED_StmtMatch - if all(version.is_python2() for version in target_versions): -+ if all((i for i in [])): ++ if all( ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++ ): # Python 2-only code, so try Python 2 grammars. return [ # Python 2.7 with future print_function import -@@ -41,13 +37,11 @@ +@@ -41,13 +39,11 @@ re.match() match = a with match() as match: @@ -166,7 +168,7 @@ with match() as match: self.assertIs(x, False) self.assertEqual(y, 0) self.assertIs(z, x) -@@ -72,16 +66,12 @@ +@@ -72,16 +68,12 @@ def test_patma_155(self): x = 0 y = None @@ -185,7 +187,7 @@ with match() as match: # At least one of the above branches must have been taken, because every Python # version has exactly one of the two 'ASYNC_*' flags -@@ -91,7 +81,7 @@ +@@ -91,7 +83,7 @@ def lib2to3_parse(src_txt: str, target_versions: Iterable[TargetVersion] = ()) -> Node: """Given a string with source, return the lib2to3 Node.""" if not src_txt.endswith("\n"): @@ -194,7 +196,7 @@ with match() as match: grammars = get_grammars(set(target_versions)) -@@ -99,9 +89,9 @@ +@@ -99,9 +91,9 @@ re.match() match = a with match() as match: @@ -238,7 +240,9 @@ def get_grammars(target_versions: Set[TargetVersion]) -> List[Grammar]: NOT_YET_IMPLEMENTED_StmtMatch - if all((i for i in [])): + if all( + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) + ): # Python 2-only code, so try Python 2 grammars. return [ # Python 2.7 with future print_function import diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap index 64f4277f8b..974a2dc217 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pep_572_py310.py.snap @@ -27,7 +27,7 @@ f(x, (a := b + c for c in range(10)), y=z, **q) ```diff --- Black +++ Ruff -@@ -1,15 +1,15 @@ +@@ -1,15 +1,20 @@ # Unparenthesized walruses are now allowed in indices since Python 3.10. -x[a:=0] -x[a:=0, b:=1] @@ -38,7 +38,7 @@ f(x, (a := b + c for c in range(10)), y=z, **q) # Walruses are allowed inside generator expressions on function calls since 3.10. -if any(match := pattern_error.match(s) for s in buffer): -+if any((i for i in [])): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): if match.group(2) == data_not_available: # Error OK to ignore. pass @@ -47,10 +47,15 @@ f(x, (a := b + c for c in range(10)), y=z, **q) -f((a := b + c for c in range(10)), x) -f(y=(a := b + c for c in range(10))) -f(x, (a := b + c for c in range(10)), y=z, **q) -+f((i for i in [])) -+f((i for i in []), x) -+f(y=(i for i in [])) -+f(x, (i for i in []), y=z, **q) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) ++f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) ++f( ++ x, ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), ++ y=z, ++ **q, ++) ``` ## Ruff Output @@ -62,15 +67,20 @@ x[NOT_YET_IMPLEMENTED_ExprNamedExpr, NOT_YET_IMPLEMENTED_ExprNamedExpr] x[5, NOT_YET_IMPLEMENTED_ExprNamedExpr] # Walruses are allowed inside generator expressions on function calls since 3.10. -if any((i for i in [])): +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): if match.group(2) == data_not_available: # Error OK to ignore. pass -f((i for i in [])) -f((i for i in []), x) -f(y=(i for i in [])) -f(x, (i for i in []), y=z, **q) +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), x) +f(y=(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])) +f( + x, + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []), + y=z, + **q, +) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap index 47302f6806..785d395e09 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_37__python37.py.snap @@ -47,7 +47,7 @@ def make_arange(n): def f(): - return (i * 2 async for i in arange(42)) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def g(): @@ -55,7 +55,7 @@ def make_arange(n): - something_long * something_long - async for something_long in async_generator(with_an_argument) - ) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) async def func(): @@ -66,17 +66,17 @@ def make_arange(n): - self.async_inc, arange(8), batch_size=3 - ) - ] -+ out_batched = [i for i in []] ++ out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] def awaited_generator_value(n): - return (await awaitable for awaitable in awaitable_list) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def make_arange(n): - return (i * 2 for i in range(n) if await wrap(i)) -+ return (i for i in []) ++ return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ``` ## Ruff Output @@ -86,24 +86,24 @@ def make_arange(n): def f(): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def g(): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) async def func(): if test: - out_batched = [i for i in []] + out_batched = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] def awaited_generator_value(n): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) def make_arange(n): - return (i for i in []) + return (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ``` ## Black Output diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap index 5e1fa92d6b..a5be560338 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_38__pep_572.py.snap @@ -76,7 +76,7 @@ while x := f(x): -y0 = (y1 := f(x)) -foo(x=(y := f(x))) +[NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] -+filtered_data = [i for i in []] ++filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +(NOT_YET_IMPLEMENTED_ExprNamedExpr) +y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr +foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) @@ -117,7 +117,7 @@ while x := f(x): +len(NOT_YET_IMPLEMENTED_ExprNamedExpr) +foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") +foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) -+if any((i for i in [])): ++if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): print(longline) -if env_base := os.environ.get("PYTHONUSERBASE", None): +if NOT_YET_IMPLEMENTED_ExprNamedExpr: @@ -150,7 +150,7 @@ if (NOT_YET_IMPLEMENTED_ExprNamedExpr) is None: if NOT_YET_IMPLEMENTED_ExprNamedExpr: pass [NOT_YET_IMPLEMENTED_ExprNamedExpr, y**2, y**3] -filtered_data = [i for i in []] +filtered_data = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] (NOT_YET_IMPLEMENTED_ExprNamedExpr) y0 = NOT_YET_IMPLEMENTED_ExprNamedExpr foo(x=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) @@ -176,7 +176,7 @@ x = NOT_YET_IMPLEMENTED_ExprNamedExpr len(NOT_YET_IMPLEMENTED_ExprNamedExpr) foo(NOT_YET_IMPLEMENTED_ExprNamedExpr, cat="vector") foo(cat=(NOT_YET_IMPLEMENTED_ExprNamedExpr)) -if any((i for i in [])): +if any((NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in [])): print(longline) if NOT_YET_IMPLEMENTED_ExprNamedExpr: return env_base diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap index 0b4edc1d0f..23020bfa28 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap @@ -248,9 +248,9 @@ instruction()#comment with bad spacing - # right - if element is not None - ] -+ lcomp = [i for i in []] -+ lcomp2 = [i for i in []] -+ lcomp3 = [i for i in []] ++ lcomp = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ lcomp2 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++ lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] while True: if False: continue @@ -404,9 +404,9 @@ short # yup arg3=True, ) - lcomp = [i for i in []] - lcomp2 = [i for i in []] - lcomp3 = [i for i in []] + lcomp = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + lcomp2 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] + lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] while True: if False: continue diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap index a656b77f09..a79cf2b66b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap @@ -72,7 +72,7 @@ def func(): - # right - if element is not None - ] -+ lcomp3 = [i for i in []] ++ lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): embedded = [] @@ -98,7 +98,7 @@ def func(): x = """ a really long string """ - lcomp3 = [i for i in []] + lcomp3 = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): embedded = [] diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap index 86ac68c37e..729478a04b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__expression.py.snap @@ -384,10 +384,10 @@ last_call() +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} +{NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -+[i for i in []] -+[i for i in []] -+[i for i in []] -+[i for i in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} +{NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -486,10 +486,10 @@ last_call() -((i**2) for i, _ in ((1, "a"), (2, "b"), (3, "c"))) -(((i**2) + j) for i in (1, 2, 3) for j in (1, 2, 3)) -(*starred,) -+(i for i in []) -+(i for i in []) -+(i for i in []) -+(i for i in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) ++(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", @@ -507,7 +507,7 @@ last_call() ) what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set( vars_to_remove --) + ) -result = ( - session.query(models.Customer.id) - .filter( @@ -515,7 +515,7 @@ last_call() - ) - .order_by(models.Customer.id.asc()) - .all() - ) +-) -result = ( - session.query(models.Customer.id) - .filter( @@ -537,7 +537,7 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -237,29 +231,27 @@ +@@ -237,29 +231,29 @@ def gen(): @@ -574,11 +574,13 @@ last_call() for y in (): ... -for z in (i for i in (1, 2, 3)): -+for z in (i for i in []): ++for ( ++ z ++) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ... for i in call(): ... -@@ -328,13 +320,18 @@ +@@ -328,13 +322,18 @@ ): return True if ( @@ -600,7 +602,7 @@ last_call() ^ aaaaaaaa.i << aaaaaaaa.k >> aaaaaaaa.l**aaaaaaaa.m // aaaaaaaa.n ): return True -@@ -342,7 +339,8 @@ +@@ -342,7 +341,8 @@ ~aaaaaaaaaaaaaaaa.a + aaaaaaaaaaaaaaaa.b - aaaaaaaaaaaaaaaa.c * aaaaaaaaaaaaaaaa.d @ aaaaaaaaaaaaaaaa.e @@ -714,10 +716,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} -[i for i in []] -[i for i in []] -[i for i in []] -[i for i in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +[NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} @@ -802,10 +804,10 @@ NOT_IMPLEMENTED_true if NOT_IMPLEMENTED_cond else NOT_IMPLEMENTED_false (SomeName) SomeName (Good, Bad, Ugly) -(i for i in []) -(i for i in []) -(i for i in []) -(i for i in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +(NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) (*NOT_YET_IMPLEMENTED_ExprStarred,) { "id": "1", @@ -868,7 +870,9 @@ for (x,) in (1,), (2,), (3,): ... for y in (): ... -for z in (i for i in []): +for ( + z +) in (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): ... for i in call(): ... diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap index 2a803075f6..40cab8fa6b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__power_op_spacing.py.snap @@ -87,7 +87,7 @@ return np.divide( i = funcs.f() ** 5 j = super().name ** 5 -k = [(2**idx, value) for idx, value in pairs] -+k = [i for i in []] ++k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2**63], [1, 2**63])] n = count <= 10**5 @@ -95,7 +95,7 @@ return np.divide( -p = {(k, k**2): v**2 for k, v in pairs} -q = [10**i for i in range(6)] +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] ++q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] r = x**y a = 5.0**~4.0 @@ -110,7 +110,7 @@ return np.divide( i = funcs.f() ** 5.0 j = super().name ** 5.0 -k = [(2.0**idx, value) for idx, value in pairs] -+k = [i for i in []] ++k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2.0**63.0], [1.0, 2**63.0])] n = count <= 10**5.0 @@ -118,7 +118,7 @@ return np.divide( -p = {(k, k**2): v**2.0 for k, v in pairs} -q = [10.5**i for i in range(6)] +p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -+q = [i for i in []] ++q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) @@ -164,13 +164,13 @@ g = a.b**c.d h = 5 ** funcs.f() i = funcs.f() ** 5 j = super().name ** 5 -k = [i for i in []] +k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2**63], [1, 2**63])] n = count <= 10**5 o = settings(max_examples=10**6) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] r = x**y a = 5.0**~4.0 @@ -183,13 +183,13 @@ g = a.b**c.d h = 5.0 ** funcs.f() i = funcs.f() ** 5.0 j = super().name ** 5.0 -k = [i for i in []] +k = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] l = mod.weights_[0] == pytest.approx(0.95**100, abs=0.001) m = [([2.0**63.0], [1.0, 2**63.0])] n = count <= 10**5.0 o = settings(max_examples=10**6.0) p = {NOT_IMPLEMENTED_dict_key: NOT_IMPLEMENTED_dict_value for key, value in NOT_IMPLEMENTED_dict} -q = [i for i in []] +q = [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap index bf646fcda1..cfa0bebf04 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__slices.py.snap @@ -43,7 +43,7 @@ ham[lower + offset : upper + offset] ```diff --- Black +++ Ruff -@@ -4,28 +4,28 @@ +@@ -4,28 +4,34 @@ slice[d::d] slice[0] slice[-1] @@ -65,13 +65,19 @@ ham[lower + offset : upper + offset] slice[not so_simple : 1 < val <= 10] -slice[(1 for i in range(42)) : x] -slice[:: [i for i in range(42)]] -+slice[(i for i in []) : x] -+slice[ :: [i for i in []]] ++slice[ ++ (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x ++] ++slice[ ++ :: [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] ++] async def f(): - slice[await x : [i async for i in arange(42)] : 42] -+ slice[await x : [i for i in []] : 42] ++ slice[ ++ await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 ++ ] # These are from PEP-8: @@ -103,12 +109,18 @@ slice[lambda x: True : lambda x: True] slice[lambda x: True :, None::] slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] -slice[(i for i in []) : x] -slice[ :: [i for i in []]] +slice[ + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) : x +] +slice[ + :: [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +] async def f(): - slice[await x : [i for i in []] : 42] + slice[ + await x : [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] : 42 + ] # These are from PEP-8: diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 8b6f78bd10..4e4a09fe83 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -277,8 +277,14 @@ aaaaaaaaaaaaaa + { dddddddddddddddd, eeeeeee, } -aaaaaaaaaaaaaa + [i for i in []] -aaaaaaaaaaaaaa + (i for i in []) +( + aaaaaaaaaaaaaa + + [NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []] +) +( + aaaaaaaaaaaaaa + + (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []) +) aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_set} # Wraps it in parentheses if it needs to break both left and right diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap index 8f54c3461d..0dd8743d48 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -121,7 +121,7 @@ with ( # currently unparsable by black: https://github.com/psf/black/issues/3678 -with (i for i in []): +with (NOT_YET_IMPLEMENTED_generator_key for NOT_YET_IMPLEMENTED_generator_key in []): pass with (a, *NOT_YET_IMPLEMENTED_ExprStarred): pass From 7ac9e0252e3c278078e19a7053671fdc845986af Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 3 Jul 2023 11:22:19 +0200 Subject: [PATCH 11/27] Document Checking formatter stability and panics (#5415) This adds the documentation, but ideally we should add the CI first --- crates/ruff_python_formatter/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/ruff_python_formatter/README.md b/crates/ruff_python_formatter/README.md index 0dde0b475f..b115eb1754 100644 --- a/crates/ruff_python_formatter/README.md +++ b/crates/ruff_python_formatter/README.md @@ -241,6 +241,29 @@ The origin of Ruff's formatter is the [Rome formatter](https://github.com/rome/t e.g. the ruff_formatter crate is forked from the [rome_formatter crate](https://github.com/rome/tools/tree/main/crates/rome_formatter). The Rome repository can be a helpful reference when implementing something in the Ruff formatter +### Checking formatter stability and panics + +There are tree common problems with the formatter: The second formatting pass looks different than +the first (formatter instability or lack of idempotency), we print invalid syntax (e.g. missing +parentheses around multiline expressions) and panics (mostly in debug assertions). We test for all +of these using the `check-formatter-stability` subcommand in `ruff_dev` + +The easiest is to check CPython: + +```shell +git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff/resources/test/cpython +cargo run --bin ruff_dev -- check-formatter-stability crates/ruff/resources/test/cpython +``` + +It is also possible large number of repositories using ruff. This dataset is large (~60GB), so we +only do this occasionally: + +```shell +curl https://raw.githubusercontent.com/akx/ruff-usage-aggregate/master/data/known-github-tomls.jsonl > github_search.jsonl +python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true) +cargo run --bin ruff_dev -- check-formatter-stability --multi-project target/checkouts +``` + ## The orphan rules and trait structure For the formatter, we would like to implement `Format` from the rust_formatter crate for all AST From dc072537e513912eced43c24e6fc8d61d77aca4d Mon Sep 17 00:00:00 2001 From: Louis Dispa Date: Mon, 3 Jul 2023 16:07:57 +0200 Subject: [PATCH 12/27] Fix python_formatter generate.py with rust path (#5475) ## Summary This PR fix an issue with the `generate.py` file of the python formatter. Since https://github.com/astral-sh/ruff/pull/5369 the [node.rs file](https://github.com/astral-sh/ruff/blob/f51dc204979964b7a7d9a2469c0c1b0c06d26980/crates/ruff_python_ast/src/node.rs) used to generate the types now has `ast::` in the enum. ```rust pub enum AnyNode { ModModule(ModModule), ModInteractive(ModInteractive), ModExpression(ModExpression), ModFunctionType(ModFunctionType), ... ``` And now: ```rust pub enum AnyNode { ModModule(ast::ModModule), ModInteractive(ast::ModInteractive), ModExpression(ast::ModExpression), ModFunctionType(ast::ModFunctionType), ... ``` The python script was not parsing rust paths. This PR adds the possibility to have it. ## Test Plan This was tested locally. ### Script output Before ``` ['ast::ModModule),', 'ast::ModInteractive),', 'ast::ModExpression),', 'ast::ModFunctionType),', 'ast::StmtFunctionDef),', 'ast::StmtAsyncFunctionDef),', 'ast::StmtClassDef),', 'ast::StmtReturn),', 'ast::StmtDelete),', 'ast::StmtAssign),', 'ast::StmtAugAssign),', 'ast::StmtAnnAssign),', 'ast::StmtFor),', 'ast::StmtAsyncFor),', 'ast::StmtWhile),', 'ast::StmtIf),', 'ast::StmtWith),', 'ast::StmtAsyncWith),', 'ast::StmtMatch),', 'ast::StmtRaise),', 'ast::StmtTry),', 'ast::StmtTryStar),', 'ast::StmtAssert),', 'ast::StmtImport),', 'ast::StmtImportFrom),', 'ast::StmtGlobal),', 'ast::StmtNonlocal),', 'ast::StmtExpr),', 'ast::StmtPass),', 'ast::StmtBreak),', 'ast::StmtContinue),', 'ast::ExprBoolOp),', 'ast::ExprNamedExpr),', 'ast::ExprBinOp),', 'ast::ExprUnaryOp),', 'ast::ExprLambda),', 'ast::ExprIfExp),', 'ast::ExprDict),', 'ast::ExprSet),', 'ast::ExprListComp),', 'ast::ExprSetComp),', 'ast::ExprDictComp),', 'ast::ExprGeneratorExp),', 'ast::ExprAwait),', 'ast::ExprYield),', 'ast::ExprYieldFrom),', 'ast::ExprCompare),', 'ast::ExprCall),', 'ast::ExprFormattedValue),', 'ast::ExprJoinedStr),', 'ast::ExprConstant),', 'ast::ExprAttribute),', 'ast::ExprSubscript),', 'ast::ExprStarred),', 'ast::ExprName),', 'ast::ExprList),', 'ast::ExprTuple),', 'ast::ExprSlice),', 'ast::ExceptHandlerExceptHandler),', 'ast::PatternMatchValue),', 'ast::PatternMatchSingleton),', 'ast::PatternMatchSequence),', 'ast::PatternMatchMapping),', 'ast::PatternMatchClass),', 'ast::PatternMatchStar),', 'ast::PatternMatchAs),', 'ast::PatternMatchOr),', 'ast::TypeIgnoreTypeIgnore),', 'Comprehension),', 'Arguments),', 'Arg),', 'ArgWithDefault),', 'Keyword),', 'Alias),', 'WithItem),', 'MatchCase),', 'Decorator),'] error: unexpected closing delimiter: `)` --> :3:55 | 2 | use ruff_formatter::{write, Buffer, FormatResult}; | - this opening brace... - ...matches this closing brace 3 | use rustpython_parser::ast::ast::ModModule),; | ^ unexpected closing delimiter Traceback (most recent call last): File "/Users/ldispa/Documents/perso/ruff/crates/ruff_python_formatter/generate.py", line 100, in node_path.write_text(rustfmt(code)) ^^^^^^^^^^^^^ File "/Users/ldispa/Documents/perso/ruff/crates/ruff_python_formatter/generate.py", line 12, in rustfmt return check_output(["rustfmt", "--emit=stdout"], input=code, text=True) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/subprocess.py", line 466, in check_output return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/homebrew/Cellar/python@3.11/3.11.4_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/subprocess.py", line 571, in run raise CalledProcessError(retcode, process.args, subprocess.CalledProcessError: Command '['rustfmt', '--emit=stdout']' returned non-zero exit status 1. ``` After: ``` ['ModModule', 'ModInteractive', 'ModExpression', 'ModFunctionType', 'StmtFunctionDef', 'StmtAsyncFunctionDef', 'StmtClassDef', 'StmtReturn', 'StmtDelete', 'StmtAssign', 'StmtAugAssign', 'StmtAnnAssign', 'StmtFor', 'StmtAsyncFor', 'StmtWhile', 'StmtIf', 'StmtWith', 'StmtAsyncWith', 'StmtMatch', 'StmtRaise', 'StmtTry', 'StmtTryStar', 'StmtAssert', 'StmtImport', 'StmtImportFrom', 'StmtGlobal', 'StmtNonlocal', 'StmtExpr', 'StmtPass', 'StmtBreak', 'StmtContinue', 'ExprBoolOp', 'ExprNamedExpr', 'ExprBinOp', 'ExprUnaryOp', 'ExprLambda', 'ExprIfExp', 'ExprDict', 'ExprSet', 'ExprListComp', 'ExprSetComp', 'ExprDictComp', 'ExprGeneratorExp', 'ExprAwait', 'ExprYield', 'ExprYieldFrom', 'ExprCompare', 'ExprCall', 'ExprFormattedValue', 'ExprJoinedStr', 'ExprConstant', 'ExprAttribute', 'ExprSubscript', 'ExprStarred', 'ExprName', 'ExprList', 'ExprTuple', 'ExprSlice', 'ExceptHandlerExceptHandler', 'PatternMatchValue', 'PatternMatchSingleton', 'PatternMatchSequence', 'PatternMatchMapping', 'PatternMatchClass', 'PatternMatchStar', 'PatternMatchAs', 'PatternMatchOr', 'TypeIgnoreTypeIgnore', 'Comprehension', 'Arguments', 'Arg', 'ArgWithDefault', 'Keyword', 'Alias', 'WithItem', 'MatchCase', 'Decorator'] ``` --- crates/ruff_python_formatter/generate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index f4e83d2edf..bcbf59871a 100644 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -28,7 +28,10 @@ nodes_file = ( node_lines = ( nodes_file.split("pub enum AnyNode {")[1].split("}")[0].strip().splitlines() ) -nodes = [node_line.split("(")[1].split("<")[0] for node_line in node_lines] +nodes = [ + node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0] + for node_line in node_lines +] print(nodes) # %% From 1e4b88969cbfa866c0ac6aace562699c1fd6b371 Mon Sep 17 00:00:00 2001 From: Harutaka Kawamura Date: Mon, 3 Jul 2023 23:11:09 +0900 Subject: [PATCH 13/27] Fix `unnecessary-encode-utf8` to fix `encode` on parenthesized strings correctly (#5478) ## Summary Fixes #5477 ## Test Plan New test cases. --- .../test/fixtures/pyupgrade/UP012.py | 5 ++ .../rules/unnecessary_encode_utf8.rs | 8 +- ...ff__rules__pyupgrade__tests__UP012.py.snap | 79 +++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py index 266e8431cc..879f3842ad 100644 --- a/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py +++ b/crates/ruff/resources/test/fixtures/pyupgrade/UP012.py @@ -70,3 +70,8 @@ print("foo".encode()) # print(b"foo") "abc" "def" )).encode() + +(f"foo{bar}").encode("utf-8") +(f"foo{bar}").encode(encoding="utf-8") +("unicode text©").encode("utf-8") +("unicode text©").encode(encoding="utf-8") diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs index 3c021a7e45..c210724341 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_encode_utf8.rs @@ -191,7 +191,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, @@ -213,7 +213,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, @@ -242,7 +242,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), kwarg.range(), args, kwargs, @@ -264,7 +264,7 @@ pub(crate) fn unnecessary_encode_utf8( diagnostic.try_set_fix(|| { remove_argument( checker.locator, - func.start(), + func.end(), arg.range(), args, kwargs, diff --git a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap index 98d0ed5f7a..d25beae3d4 100644 --- a/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap +++ b/crates/ruff/src/rules/pyupgrade/snapshots/ruff__rules__pyupgrade__tests__UP012.py.snap @@ -452,6 +452,8 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 71 | | "def" 72 | | )).encode() | |___________^ UP012 +73 | +74 | (f"foo{bar}").encode("utf-8") | = help: Rewrite as bytes literal @@ -465,5 +467,82 @@ UP012.py:69:1: UP012 [*] Unnecessary call to `encode` as UTF-8 70 |+ b"abc" 71 |+ b"def" 72 |+)) +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") + +UP012.py:74:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +72 | )).encode() +73 | +74 | (f"foo{bar}").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +71 71 | "def" +72 72 | )).encode() +73 73 | +74 |-(f"foo{bar}").encode("utf-8") + 74 |+(f"foo{bar}").encode() +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:75:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +72 72 | )).encode() +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 |-(f"foo{bar}").encode(encoding="utf-8") + 75 |+(f"foo{bar}").encode() +76 76 | ("unicode text©").encode("utf-8") +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:76:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +74 | (f"foo{bar}").encode("utf-8") +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 +77 | ("unicode text©").encode(encoding="utf-8") + | + = help: Remove unnecessary encoding argument + +ℹ Fix +73 73 | +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 |-("unicode text©").encode("utf-8") + 76 |+("unicode text©").encode() +77 77 | ("unicode text©").encode(encoding="utf-8") + +UP012.py:77:1: UP012 [*] Unnecessary call to `encode` as UTF-8 + | +75 | (f"foo{bar}").encode(encoding="utf-8") +76 | ("unicode text©").encode("utf-8") +77 | ("unicode text©").encode(encoding="utf-8") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP012 + | + = help: Remove unnecessary encoding argument + +ℹ Fix +74 74 | (f"foo{bar}").encode("utf-8") +75 75 | (f"foo{bar}").encode(encoding="utf-8") +76 76 | ("unicode text©").encode("utf-8") +77 |-("unicode text©").encode(encoding="utf-8") + 77 |+("unicode text©").encode() From d2450c25abc428cb5276933b2ae21017fcd98f3d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 12:21:01 -0400 Subject: [PATCH 14/27] Audit `remove_argument` usages to use end-of-function (#5480) ## Summary This PR applies the fix in #5478 to a variety of other call-sites, and fixes some other range hygienic stuff in the rules that were modified. --- .../test/fixtures/pandas_vet/PD002.py | 9 +- crates/ruff/src/checkers/ast/mod.rs | 19 +-- .../rules/model_without_dunder_str.rs | 23 ++- ..._flake8_django__tests__DJ008_DJ008.py.snap | 64 ++----- .../flake8_pytest_style/rules/fixture.rs | 135 +++++++-------- crates/ruff/src/rules/pandas_vet/fixes.rs | 44 ----- crates/ruff/src/rules/pandas_vet/mod.rs | 1 - .../pandas_vet/rules/inplace_argument.rs | 77 +++++---- .../src/rules/pandas_vet/rules/pd_merge.rs | 8 +- ...es__pandas_vet__tests__PD002_PD002.py.snap | 156 ++++++++++-------- .../pyupgrade/rules/replace_stdout_stderr.rs | 2 +- .../rules/unnecessary_class_parentheses.rs | 11 +- .../rules/useless_object_inheritance.rs | 11 +- 13 files changed, 245 insertions(+), 315 deletions(-) delete mode 100644 crates/ruff/src/rules/pandas_vet/fixes.rs diff --git a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py index 99dc33a327..4d1fc96b59 100644 --- a/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py +++ b/crates/ruff/resources/test/fixtures/pandas_vet/PD002.py @@ -4,7 +4,9 @@ x = pd.DataFrame() x.drop(["a"], axis=1, inplace=True) -x.drop(["a"], axis=1, inplace=True) +x.y.drop(["a"], axis=1, inplace=True) + +x["y"].drop(["a"], axis=1, inplace=True) x.drop( inplace=True, @@ -23,6 +25,7 @@ x.drop(["a"], axis=1, **kwargs, inplace=True) x.drop(["a"], axis=1, inplace=True, **kwargs) f(x.drop(["a"], axis=1, inplace=True)) -x.apply(lambda x: x.sort_values('a', inplace=True)) +x.apply(lambda x: x.sort_values("a", inplace=True)) import torch -torch.m.ReLU(inplace=True) # safe because this isn't a pandas call + +torch.m.ReLU(inplace=True) # safe because this isn't a pandas call diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 8d85f2968c..fa49c24332 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -668,21 +668,17 @@ where } if !self.is_stub { if self.enabled(Rule::DjangoModelWithoutDunderStr) { - if let Some(diagnostic) = - flake8_django::rules::model_without_dunder_str(self, bases, body, stmt) - { - self.diagnostics.push(diagnostic); - } + flake8_django::rules::model_without_dunder_str(self, class_def); } } if self.enabled(Rule::GlobalStatement) { pylint::rules::global_statement(self, name); } if self.enabled(Rule::UselessObjectInheritance) { - pyupgrade::rules::useless_object_inheritance(self, class_def, stmt); + pyupgrade::rules::useless_object_inheritance(self, class_def); } if self.enabled(Rule::UnnecessaryClassParentheses) { - pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt); + pyupgrade::rules::unnecessary_class_parentheses(self, class_def); } if self.enabled(Rule::AmbiguousClassName) { if let Some(diagnostic) = @@ -2756,17 +2752,12 @@ where flake8_debugger::rules::debugger_call(self, expr, func); } if self.enabled(Rule::PandasUseOfInplaceArgument) { - self.diagnostics.extend( - pandas_vet::rules::inplace_argument(self, expr, func, args, keywords) - .into_iter(), - ); + pandas_vet::rules::inplace_argument(self, expr, func, args, keywords); } pandas_vet::rules::call(self, func); if self.enabled(Rule::PandasUseOfPdMerge) { - if let Some(diagnostic) = pandas_vet::rules::use_of_pd_merge(func) { - self.diagnostics.push(diagnostic); - }; + pandas_vet::rules::use_of_pd_merge(self, func); } if self.enabled(Rule::CallDatetimeWithoutTzinfo) { flake8_datetimez::rules::call_datetime_without_tzinfo( diff --git a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs index 31d0ea65a9..6c30cb52c1 100644 --- a/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs +++ b/crates/ruff/src/rules/flake8_django/rules/model_without_dunder_str.rs @@ -52,21 +52,20 @@ impl Violation for DjangoModelWithoutDunderStr { /// DJ008 pub(crate) fn model_without_dunder_str( - checker: &Checker, - bases: &[Expr], - body: &[Stmt], - class_location: &Stmt, -) -> Option { + checker: &mut Checker, + ast::StmtClassDef { + name, bases, body, .. + }: &ast::StmtClassDef, +) { if !is_non_abstract_model(bases, body, checker.semantic()) { - return None; + return; } - if !has_dunder_method(body) { - return Some(Diagnostic::new( - DjangoModelWithoutDunderStr, - class_location.range(), - )); + if has_dunder_method(body) { + return; } - None + checker + .diagnostics + .push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range())); } fn has_dunder_method(body: &[Stmt]) -> bool { diff --git a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap index 1af01e2856..2aae3d948b 100644 --- a/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap +++ b/crates/ruff/src/rules/flake8_django/snapshots/ruff__rules__flake8_django__tests__DJ008_DJ008.py.snap @@ -1,58 +1,26 @@ --- source: crates/ruff/src/rules/flake8_django/mod.rs --- -DJ008.py:6:1: DJ008 Model does not define `__str__` method +DJ008.py:6:7: DJ008 Model does not define `__str__` method + | +5 | # Models without __str__ +6 | class TestModel1(models.Model): + | ^^^^^^^^^^ DJ008 +7 | new_field = models.CharField(max_length=10) + | + +DJ008.py:21:7: DJ008 Model does not define `__str__` method | - 5 | # Models without __str__ - 6 | / class TestModel1(models.Model): - 7 | | new_field = models.CharField(max_length=10) - 8 | | - 9 | | class Meta: -10 | | verbose_name = "test model" -11 | | verbose_name_plural = "test models" -12 | | -13 | | @property -14 | | def my_brand_new_property(self): -15 | | return 1 -16 | | -17 | | def my_beautiful_method(self): -18 | | return 2 - | |________________^ DJ008 +21 | class TestModel2(Model): + | ^^^^^^^^^^ DJ008 +22 | new_field = models.CharField(max_length=10) | -DJ008.py:21:1: DJ008 Model does not define `__str__` method +DJ008.py:36:7: DJ008 Model does not define `__str__` method | -21 | / class TestModel2(Model): -22 | | new_field = models.CharField(max_length=10) -23 | | -24 | | class Meta: -25 | | verbose_name = "test model" -26 | | verbose_name_plural = "test models" -27 | | -28 | | @property -29 | | def my_brand_new_property(self): -30 | | return 1 -31 | | -32 | | def my_beautiful_method(self): -33 | | return 2 - | |________________^ DJ008 - | - -DJ008.py:36:1: DJ008 Model does not define `__str__` method - | -36 | / class TestModel3(Model): -37 | | new_field = models.CharField(max_length=10) -38 | | -39 | | class Meta: -40 | | abstract = False -41 | | -42 | | @property -43 | | def my_brand_new_property(self): -44 | | return 1 -45 | | -46 | | def my_beautiful_method(self): -47 | | return 2 - | |________________^ DJ008 +36 | class TestModel3(Model): + | ^^^^^^^^^^ DJ008 +37 | new_field = models.CharField(max_length=10) | diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs index 7cf8ff64f9..dc840d42f5 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -1,9 +1,8 @@ use std::fmt; -use anyhow::Result; -use ruff_text_size::{TextLen, TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange}; use rustpython_parser::ast::Decorator; -use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Keyword, Ranged, Stmt}; +use rustpython_parser::ast::{self, ArgWithDefault, Arguments, Expr, Ranged, Stmt}; use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; @@ -11,7 +10,6 @@ use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; use ruff_python_ast::helpers::collect_arg_names; use ruff_python_ast::identifier::Identifier; -use ruff_python_ast::source_code::Locator; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; use ruff_python_semantic::analyze::visibility::is_abstract; @@ -25,21 +23,6 @@ use super::helpers::{ get_mark_decorators, is_pytest_fixture, is_pytest_yield_fixture, keyword_is_literal, }; -#[derive(Debug, PartialEq, Eq)] -pub(crate) enum Parentheses { - None, - Empty, -} - -impl fmt::Display for Parentheses { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Parentheses::None => fmt.write_str(""), - Parentheses::Empty => fmt.write_str("()"), - } - } -} - #[violation] pub struct PytestFixtureIncorrectParenthesesStyle { expected: Parentheses, @@ -196,8 +179,23 @@ impl AlwaysAutofixableViolation for PytestUnnecessaryAsyncioMarkOnFixture { } } -#[derive(Default)] +#[derive(Debug, PartialEq, Eq)] +enum Parentheses { + None, + Empty, +} + +impl fmt::Display for Parentheses { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Parentheses::None => fmt.write_str(""), + Parentheses::Empty => fmt.write_str("()"), + } + } +} + /// Visitor that skips functions +#[derive(Debug, Default)] struct SkipFunctionsVisitor<'a> { has_return_with_value: bool, has_yield_from: bool, @@ -245,7 +243,7 @@ where } } -fn get_fixture_decorator<'a>( +fn fixture_decorator<'a>( decorators: &'a [Decorator], semantic: &SemanticModel, ) -> Option<&'a Decorator> { @@ -271,16 +269,6 @@ fn pytest_fixture_parentheses( checker.diagnostics.push(diagnostic); } -pub(crate) fn fix_extraneous_scope_function( - locator: &Locator, - stmt_at: TextSize, - expr_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Result { - remove_argument(locator, stmt_at, expr_range, args, keywords, false) -} - /// PT001, PT002, PT003 fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &Decorator) { match &decorator.expression { @@ -290,28 +278,31 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D keywords, range: _, }) => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && !checker.settings.flake8_pytest_style.fixture_parentheses - && args.is_empty() - && keywords.is_empty() - { - let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::None, - Parentheses::Empty, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if !checker.settings.flake8_pytest_style.fixture_parentheses + && args.is_empty() + && keywords.is_empty() + { + let fix = Fix::automatic(Edit::deletion(func.end(), decorator.end())); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::None, + Parentheses::Empty, + ); + } } - if checker.enabled(Rule::PytestFixturePositionalArgs) && !args.is_empty() { - checker.diagnostics.push(Diagnostic::new( - PytestFixturePositionalArgs { - function: func_name.to_string(), - }, - decorator.range(), - )); + if checker.enabled(Rule::PytestFixturePositionalArgs) { + if !args.is_empty() { + checker.diagnostics.push(Diagnostic::new( + PytestFixturePositionalArgs { + function: func_name.to_string(), + }, + decorator.range(), + )); + } } if checker.enabled(Rule::PytestExtraneousScopeFunction) { @@ -324,16 +315,16 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D let mut diagnostic = Diagnostic::new(PytestExtraneousScopeFunction, scope_keyword.range()); if checker.patch(diagnostic.kind.rule()) { - let expr_range = diagnostic.range(); - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - fix_extraneous_scope_function( + diagnostic.try_set_fix(|| { + remove_argument( checker.locator, - decorator.start(), - expr_range, + func.end(), + scope_keyword.range, args, keywords, + false, ) + .map(Fix::suggested) }); } checker.diagnostics.push(diagnostic); @@ -342,20 +333,20 @@ fn check_fixture_decorator(checker: &mut Checker, func_name: &str, decorator: &D } } _ => { - if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) - && checker.settings.flake8_pytest_style.fixture_parentheses - { - let fix = Fix::automatic(Edit::insertion( - Parentheses::Empty.to_string(), - decorator.end(), - )); - pytest_fixture_parentheses( - checker, - decorator, - fix, - Parentheses::Empty, - Parentheses::None, - ); + if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) { + if checker.settings.flake8_pytest_style.fixture_parentheses { + let fix = Fix::automatic(Edit::insertion( + Parentheses::Empty.to_string(), + decorator.end(), + )); + pytest_fixture_parentheses( + checker, + decorator, + fix, + Parentheses::Empty, + Parentheses::None, + ); + } } } } @@ -511,7 +502,7 @@ pub(crate) fn fixture( decorators: &[Decorator], body: &[Stmt], ) { - let decorator = get_fixture_decorator(decorators, checker.semantic()); + let decorator = fixture_decorator(decorators, checker.semantic()); if let Some(decorator) = decorator { if checker.enabled(Rule::PytestFixtureIncorrectParenthesesStyle) || checker.enabled(Rule::PytestFixturePositionalArgs) diff --git a/crates/ruff/src/rules/pandas_vet/fixes.rs b/crates/ruff/src/rules/pandas_vet/fixes.rs deleted file mode 100644 index 8a3d368f07..0000000000 --- a/crates/ruff/src/rules/pandas_vet/fixes.rs +++ /dev/null @@ -1,44 +0,0 @@ -use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; - -use ruff_diagnostics::{Edit, Fix}; -use ruff_python_ast::source_code::Locator; - -use crate::autofix::edits::remove_argument; - -fn match_name(expr: &Expr) -> Option<&str> { - if let Expr::Call(ast::ExprCall { func, .. }) = expr { - if let Expr::Attribute(ast::ExprAttribute { value, .. }) = func.as_ref() { - if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { - return Some(id); - } - } - } - None -} - -/// Remove the `inplace` argument from a function call and replace it with an -/// assignment. -pub(super) fn convert_inplace_argument_to_assignment( - locator: &Locator, - expr: &Expr, - violation_range: TextRange, - args: &[Expr], - keywords: &[Keyword], -) -> Option { - // Add the assignment. - let name = match_name(expr)?; - let insert_assignment = Edit::insertion(format!("{name} = "), expr.start()); - - // Remove the `inplace` argument. - let remove_argument = remove_argument( - locator, - expr.start(), - violation_range, - args, - keywords, - false, - ) - .ok()?; - Some(Fix::suggested_edits(insert_assignment, [remove_argument])) -} diff --git a/crates/ruff/src/rules/pandas_vet/mod.rs b/crates/ruff/src/rules/pandas_vet/mod.rs index d3ac303cbc..2032749fe2 100644 --- a/crates/ruff/src/rules/pandas_vet/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/mod.rs @@ -1,5 +1,4 @@ //! Rules from [pandas-vet](https://pypi.org/project/pandas-vet/). -pub(crate) mod fixes; pub(crate) mod helpers; pub(crate) mod rules; diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index cf5983213d..98a44a58d6 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -1,12 +1,15 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use ruff_text_size::TextRange; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; -use ruff_diagnostics::{AutofixKind, Diagnostic, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::is_const_true; +use ruff_python_ast::source_code::Locator; use ruff_python_semantic::{BindingKind, Import}; +use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; use crate::registry::AsRule; -use crate::rules::pandas_vet::fixes::convert_inplace_argument_to_assignment; /// ## What it does /// Checks for `inplace=True` usages in `pandas` function and method @@ -50,23 +53,17 @@ impl Violation for PandasUseOfInplaceArgument { /// PD002 pub(crate) fn inplace_argument( - checker: &Checker, + checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr], keywords: &[Keyword], -) -> Option { - let mut seen_star = false; - let mut is_checkable = false; - let mut is_pandas = false; - +) { + // If the function was imported from another module, and it's _not_ Pandas, abort. if let Some(call_path) = checker.semantic().resolve_call_path(func) { - is_checkable = true; - - let module = call_path[0]; - is_pandas = checker - .semantic() - .find_binding(module) + if !call_path + .first() + .and_then(|module| checker.semantic().find_binding(module)) .map_or(false, |binding| { matches!( binding.kind, @@ -74,23 +71,20 @@ pub(crate) fn inplace_argument( qualified_name: "pandas" }) ) - }); + }) + { + return; + } } + let mut seen_star = false; for keyword in keywords.iter().rev() { let Some(arg) = &keyword.arg else { seen_star = true; continue; }; if arg == "inplace" { - let is_true_literal = match &keyword.value { - Expr::Constant(ast::ExprConstant { - value: Constant::Bool(boolean), - .. - }) => *boolean, - _ => false, - }; - if is_true_literal { + if is_const_true(&keyword.value) { let mut diagnostic = Diagnostic::new(PandasUseOfInplaceArgument, keyword.range()); if checker.patch(diagnostic.kind.rule()) { // Avoid applying the fix if: @@ -110,7 +104,7 @@ pub(crate) fn inplace_argument( if let Some(fix) = convert_inplace_argument_to_assignment( checker.locator, expr, - diagnostic.range(), + keyword.range(), args, keywords, ) { @@ -119,18 +113,35 @@ pub(crate) fn inplace_argument( } } - // Without a static type system, only module-level functions could potentially be - // non-pandas calls. If they're not, `inplace` should be considered safe. - if is_checkable && !is_pandas { - return None; - } - - return Some(diagnostic); + checker.diagnostics.push(diagnostic); } // Duplicate keywords is a syntax error, so we can stop here. break; } } - None +} + +/// Remove the `inplace` argument from a function call and replace it with an +/// assignment. +fn convert_inplace_argument_to_assignment( + locator: &Locator, + expr: &Expr, + expr_range: TextRange, + args: &[Expr], + keywords: &[Keyword], +) -> Option { + // Add the assignment. + let call = expr.as_call_expr()?; + let attr = call.func.as_attribute_expr()?; + let insert_assignment = Edit::insertion( + format!("{name} = ", name = locator.slice(attr.value.range())), + expr.start(), + ); + + // Remove the `inplace` argument. + let remove_argument = + remove_argument(locator, call.func.end(), expr_range, args, keywords, false).ok()?; + + Some(Fix::suggested_edits(insert_assignment, [remove_argument])) } diff --git a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs index b6c4120d7d..873d5c7f68 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/pd_merge.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{self, Expr, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -17,13 +18,14 @@ impl Violation for PandasUseOfPdMerge { } /// PD015 -pub(crate) fn use_of_pd_merge(func: &Expr) -> Option { +pub(crate) fn use_of_pd_merge(checker: &mut Checker, func: &Expr) { if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func { if let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() { if id == "pd" && attr == "merge" { - return Some(Diagnostic::new(PandasUseOfPdMerge, func.range())); + checker + .diagnostics + .push(Diagnostic::new(PandasUseOfPdMerge, func.range())); } } } - None } diff --git a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap index b837655f46..513c426e64 100644 --- a/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap +++ b/crates/ruff/src/rules/pandas_vet/snapshots/ruff__rules__pandas_vet__tests__PD002_PD002.py.snap @@ -8,7 +8,7 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 | x.drop(["a"], axis=1, inplace=True) | ^^^^^^^^^^^^ PD002 6 | -7 | x.drop(["a"], axis=1, inplace=True) +7 | x.y.drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -19,17 +19,17 @@ PD002.py:5:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 5 |-x.drop(["a"], axis=1, inplace=True) 5 |+x = x.drop(["a"], axis=1) 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:7:25: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | 5 | x.drop(["a"], axis=1, inplace=True) 6 | -7 | x.drop(["a"], axis=1, inplace=True) - | ^^^^^^^^^^^^ PD002 +7 | x.y.drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 8 | -9 | x.drop( +9 | x["y"].drop(["a"], axis=1, inplace=True) | = help: Assign to variable; remove `inplace` arg @@ -37,104 +37,124 @@ PD002.py:7:23: PD002 [*] `inplace=True` should be avoided; it has inconsistent b 4 4 | 5 5 | x.drop(["a"], axis=1, inplace=True) 6 6 | -7 |-x.drop(["a"], axis=1, inplace=True) - 7 |+x = x.drop(["a"], axis=1) +7 |-x.y.drop(["a"], axis=1, inplace=True) + 7 |+x.y = x.y.drop(["a"], axis=1) 8 8 | -9 9 | x.drop( -10 10 | inplace=True, +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | -PD002.py:10:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:9:28: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | - 9 | x.drop( -10 | inplace=True, - | ^^^^^^^^^^^^ PD002 -11 | columns=["a"], -12 | axis=1, + 7 | x.y.drop(["a"], axis=1, inplace=True) + 8 | + 9 | x["y"].drop(["a"], axis=1, inplace=True) + | ^^^^^^^^^^^^ PD002 +10 | +11 | x.drop( | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix 6 6 | -7 7 | x.drop(["a"], axis=1, inplace=True) +7 7 | x.y.drop(["a"], axis=1, inplace=True) 8 8 | -9 |-x.drop( -10 |- inplace=True, - 9 |+x = x.drop( -11 10 | columns=["a"], -12 11 | axis=1, -13 12 | ) +9 |-x["y"].drop(["a"], axis=1, inplace=True) + 9 |+x["y"] = x["y"].drop(["a"], axis=1) +10 10 | +11 11 | x.drop( +12 12 | inplace=True, -PD002.py:17:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:12:5: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -15 | if True: -16 | x.drop( -17 | inplace=True, +11 | x.drop( +12 | inplace=True, + | ^^^^^^^^^^^^ PD002 +13 | columns=["a"], +14 | axis=1, + | + = help: Assign to variable; remove `inplace` arg + +ℹ Suggested fix +8 8 | +9 9 | x["y"].drop(["a"], axis=1, inplace=True) +10 10 | +11 |-x.drop( +12 |- inplace=True, + 11 |+x = x.drop( +13 12 | columns=["a"], +14 13 | axis=1, +15 14 | ) + +PD002.py:19:9: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior + | +17 | if True: +18 | x.drop( +19 | inplace=True, | ^^^^^^^^^^^^ PD002 -18 | columns=["a"], -19 | axis=1, +20 | columns=["a"], +21 | axis=1, | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -13 13 | ) -14 14 | -15 15 | if True: -16 |- x.drop( -17 |- inplace=True, - 16 |+ x = x.drop( -18 17 | columns=["a"], -19 18 | axis=1, -20 19 | ) +15 15 | ) +16 16 | +17 17 | if True: +18 |- x.drop( +19 |- inplace=True, + 18 |+ x = x.drop( +20 19 | columns=["a"], +21 20 | axis=1, +22 21 | ) -PD002.py:22:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:24:33: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior | -20 | ) -21 | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) +22 | ) +23 | +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) | ^^^^^^^^^^^^ PD002 -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg ℹ Suggested fix -19 19 | axis=1, -20 20 | ) -21 21 | -22 |-x.drop(["a"], axis=1, **kwargs, inplace=True) - 22 |+x = x.drop(["a"], axis=1, **kwargs) -23 23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 24 | f(x.drop(["a"], axis=1, inplace=True)) -25 25 | +21 21 | axis=1, +22 22 | ) +23 23 | +24 |-x.drop(["a"], axis=1, **kwargs, inplace=True) + 24 |+x = x.drop(["a"], axis=1, **kwargs) +25 25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 26 | f(x.drop(["a"], axis=1, inplace=True)) +27 27 | -PD002.py:23:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:25:23: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) | ^^^^^^^^^^^^ PD002 -24 | f(x.drop(["a"], axis=1, inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:24:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:26:25: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -22 | x.drop(["a"], axis=1, **kwargs, inplace=True) -23 | x.drop(["a"], axis=1, inplace=True, **kwargs) -24 | f(x.drop(["a"], axis=1, inplace=True)) +24 | x.drop(["a"], axis=1, **kwargs, inplace=True) +25 | x.drop(["a"], axis=1, inplace=True, **kwargs) +26 | f(x.drop(["a"], axis=1, inplace=True)) | ^^^^^^^^^^^^ PD002 -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | = help: Assign to variable; remove `inplace` arg -PD002.py:26:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior +PD002.py:28:38: PD002 `inplace=True` should be avoided; it has inconsistent behavior | -24 | f(x.drop(["a"], axis=1, inplace=True)) -25 | -26 | x.apply(lambda x: x.sort_values('a', inplace=True)) +26 | f(x.drop(["a"], axis=1, inplace=True)) +27 | +28 | x.apply(lambda x: x.sort_values("a", inplace=True)) | ^^^^^^^^^^^^ PD002 -27 | import torch -28 | torch.m.ReLU(inplace=True) # safe because this isn't a pandas call +29 | import torch | = help: Assign to variable; remove `inplace` arg diff --git a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs index 01592e3c29..a94eed542e 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/replace_stdout_stderr.rs @@ -69,7 +69,7 @@ fn generate_fix( Edit::range_replacement("capture_output=True".to_string(), first.range()), [remove_argument( locator, - func.start(), + func.end(), second.range(), args, keywords, diff --git a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs index 5fcb60fa8c..69a27b27b4 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs @@ -1,11 +1,10 @@ use std::ops::Add; use ruff_text_size::{TextRange, TextSize}; -use rustpython_parser::ast::{self, Stmt}; +use rustpython_parser::ast::{self, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::identifier::Identifier; use crate::checkers::ast::Checker; use crate::registry::AsRule; @@ -44,16 +43,12 @@ impl AlwaysAutofixableViolation for UnnecessaryClassParentheses { } /// UP039 -pub(crate) fn unnecessary_class_parentheses( - checker: &mut Checker, - class_def: &ast::StmtClassDef, - stmt: &Stmt, -) { +pub(crate) fn unnecessary_class_parentheses(checker: &mut Checker, class_def: &ast::StmtClassDef) { if !class_def.bases.is_empty() || !class_def.keywords.is_empty() { return; } - let offset = stmt.identifier().start(); + let offset = class_def.name.end(); let contents = checker.locator.after(offset); // Find the open and closing parentheses between the class name and the colon, if they exist. diff --git a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs index aa6ec0e31b..b3ac4c2899 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/useless_object_inheritance.rs @@ -1,8 +1,7 @@ -use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; +use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::identifier::Identifier; use crate::autofix::edits::remove_argument; use crate::checkers::ast::Checker; @@ -47,11 +46,7 @@ impl AlwaysAutofixableViolation for UselessObjectInheritance { } /// UP004 -pub(crate) fn useless_object_inheritance( - checker: &mut Checker, - class_def: &ast::StmtClassDef, - stmt: &Stmt, -) { +pub(crate) fn useless_object_inheritance(checker: &mut Checker, class_def: &ast::StmtClassDef) { for expr in &class_def.bases { let Expr::Name(ast::ExprName { id, .. }) = expr else { continue; @@ -73,7 +68,7 @@ pub(crate) fn useless_object_inheritance( diagnostic.try_set_fix(|| { let edit = remove_argument( checker.locator, - stmt.identifier().start(), + class_def.name.end(), expr.range(), &class_def.bases, &class_def.keywords, From dadad0e9ed861b50c87f2bf5c4520b949ac177ab Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 12:21:26 -0400 Subject: [PATCH 15/27] Remove some allocations in argument detection (#5481) ## Summary Drive-by PR to remove some allocations around argument name matching. --- crates/ruff/src/checkers/ast/mod.rs | 2 +- .../rules/request_without_timeout.rs | 47 +++++----- .../rules/function_uses_loop_variable.rs | 89 ++++++++++--------- .../flake8_pytest_style/rules/fixture.rs | 4 +- .../rules/flake8_pytest_style/rules/patch.rs | 19 ++-- crates/ruff_python_ast/src/helpers.rs | 33 +++---- 6 files changed, 97 insertions(+), 97 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index fa49c24332..0d8a631558 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -72,7 +72,7 @@ pub(crate) struct Checker<'a> { deferred: Deferred<'a>, pub(crate) diagnostics: Vec, // Check-specific state. - pub(crate) flake8_bugbear_seen: Vec<&'a Expr>, + pub(crate) flake8_bugbear_seen: Vec<&'a ast::ExprName>, } impl<'a> Checker<'a> { diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs index c164dcadc1..08736d5fc9 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_without_timeout.rs @@ -1,31 +1,28 @@ -use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::SimpleCallArgs; +use ruff_python_ast::helpers::{is_const_none, SimpleCallArgs}; use crate::checkers::ast::Checker; #[violation] pub struct RequestWithoutTimeout { - pub timeout: Option, + implicit: bool, } impl Violation for RequestWithoutTimeout { #[derive_message_formats] fn message(&self) -> String { - let RequestWithoutTimeout { timeout } = self; - match timeout { - Some(value) => { - format!("Probable use of requests call with timeout set to `{value}`") - } - None => format!("Probable use of requests call without timeout"), + let RequestWithoutTimeout { implicit } = self; + if *implicit { + format!("Probable use of requests call without timeout") + } else { + format!("Probable use of requests call with timeout set to `None`") } } } -const HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; - /// S113 pub(crate) fn request_without_timeout( checker: &mut Checker, @@ -37,30 +34,26 @@ pub(crate) fn request_without_timeout( .semantic() .resolve_call_path(func) .map_or(false, |call_path| { - HTTP_VERBS - .iter() - .any(|func_name| call_path.as_slice() == ["requests", func_name]) + matches!( + call_path.as_slice(), + [ + "requests", + "get" | "options" | "head" | "post" | "put" | "patch" | "delete" + ] + ) }) { let call_args = SimpleCallArgs::new(args, keywords); - if let Some(timeout_arg) = call_args.keyword_argument("timeout") { - if let Some(timeout) = match timeout_arg { - Expr::Constant(ast::ExprConstant { - value: value @ Constant::None, - .. - }) => Some(checker.generator().constant(value)), - _ => None, - } { + if let Some(timeout) = call_args.keyword_argument("timeout") { + if is_const_none(timeout) { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { - timeout: Some(timeout), - }, - timeout_arg.range(), + RequestWithoutTimeout { implicit: false }, + timeout.range(), )); } } else { checker.diagnostics.push(Diagnostic::new( - RequestWithoutTimeout { timeout: None }, + RequestWithoutTimeout { implicit: true }, func.range(), )); } diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs index 9741b37c7e..edba6aa901 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/function_uses_loop_variable.rs @@ -1,9 +1,8 @@ -use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Comprehension, Expr, ExprContext, Ranged, Stmt}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::helpers::collect_arg_names; +use ruff_python_ast::helpers::includes_arg_name; use ruff_python_ast::types::Node; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -58,19 +57,17 @@ impl Violation for FunctionUsesLoopVariable { #[derive(Default)] struct LoadedNamesVisitor<'a> { - // Tuple of: name, defining expression, and defining range. - loaded: Vec<(&'a str, &'a Expr)>, - // Tuple of: name, defining expression, and defining range. - stored: Vec<(&'a str, &'a Expr)>, + loaded: Vec<&'a ast::ExprName>, + stored: Vec<&'a ast::ExprName>, } /// `Visitor` to collect all used identifiers in a statement. impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { - Expr::Name(ast::ExprName { id, ctx, range: _ }) => match ctx { - ExprContext::Load => self.loaded.push((id, expr)), - ExprContext::Store => self.stored.push((id, expr)), + Expr::Name(name) => match &name.ctx { + ExprContext::Load => self.loaded.push(name), + ExprContext::Store => self.stored.push(name), ExprContext::Del => {} }, _ => visitor::walk_expr(self, expr), @@ -80,7 +77,7 @@ impl<'a> Visitor<'a> for LoadedNamesVisitor<'a> { #[derive(Default)] struct SuspiciousVariablesVisitor<'a> { - names: Vec<(&'a str, &'a Expr)>, + names: Vec<&'a ast::ExprName>, safe_functions: Vec<&'a Expr>, } @@ -95,17 +92,20 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_body(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .into_iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); + return; } Stmt::Return(ast::StmtReturn { @@ -132,10 +132,9 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { }) => { match func.as_ref() { Expr::Name(ast::ExprName { id, .. }) => { - let id = id.as_str(); - if id == "filter" || id == "reduce" || id == "map" { + if matches!(id.as_str(), "filter" | "reduce" | "map") { for arg in args { - if matches!(arg, Expr::Lambda(_)) { + if arg.is_lambda_expr() { self.safe_functions.push(arg); } } @@ -159,7 +158,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { for keyword in keywords { if keyword.arg.as_ref().map_or(false, |arg| arg == "key") - && matches!(keyword.value, Expr::Lambda(_)) + && keyword.value.is_lambda_expr() { self.safe_functions.push(&keyword.value); } @@ -175,17 +174,19 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { let mut visitor = LoadedNamesVisitor::default(); visitor.visit_expr(body); - // Collect all argument names. - let mut arg_names = collect_arg_names(args); - arg_names.extend(visitor.stored.iter().map(|(id, ..)| id)); - // Treat any non-arguments as "suspicious". - self.names.extend( - visitor - .loaded - .iter() - .filter(|(id, ..)| !arg_names.contains(id)), - ); + self.names + .extend(visitor.loaded.into_iter().filter(|loaded| { + if visitor.stored.iter().any(|stored| stored.id == loaded.id) { + return false; + } + + if includes_arg_name(&loaded.id, args) { + return false; + } + + true + })); return; } @@ -198,7 +199,7 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> { #[derive(Default)] struct NamesFromAssignmentsVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all names used in an assignment expression. @@ -206,7 +207,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { fn visit_expr(&mut self, expr: &'a Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - self.names.insert(id.as_str()); + self.names.push(id.as_str()); } Expr::Starred(ast::ExprStarred { value, .. }) => { self.visit_expr(value); @@ -223,7 +224,7 @@ impl<'a> Visitor<'a> for NamesFromAssignmentsVisitor<'a> { #[derive(Default)] struct AssignedNamesVisitor<'a> { - names: FxHashSet<&'a str>, + names: Vec<&'a str>, } /// `Visitor` to collect all used identifiers in a statement. @@ -257,7 +258,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> { } fn visit_expr(&mut self, expr: &'a Expr) { - if matches!(expr, Expr::Lambda(_)) { + if expr.is_lambda_expr() { // Don't recurse. return; } @@ -300,15 +301,15 @@ pub(crate) fn function_uses_loop_variable<'a>(checker: &mut Checker<'a>, node: & // If a variable was used in a function or lambda body, and assigned in the // loop, flag it. - for (name, expr) in suspicious_variables { - if reassigned_in_loop.contains(name) { - if !checker.flake8_bugbear_seen.contains(&expr) { - checker.flake8_bugbear_seen.push(expr); + for name in suspicious_variables { + if reassigned_in_loop.contains(&name.id.as_str()) { + if !checker.flake8_bugbear_seen.contains(&name) { + checker.flake8_bugbear_seen.push(name); checker.diagnostics.push(Diagnostic::new( FunctionUsesLoopVariable { - name: name.to_string(), + name: name.id.to_string(), }, - expr.range(), + name.range(), )); } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs index dc840d42f5..70aad572bc 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/fixture.rs @@ -8,7 +8,7 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::helpers::collect_arg_names; +use ruff_python_ast::helpers::includes_arg_name; use ruff_python_ast::identifier::Identifier; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -446,7 +446,7 @@ fn check_fixture_decorator_name(checker: &mut Checker, decorator: &Decorator) { /// PT021 fn check_fixture_addfinalizer(checker: &mut Checker, args: &Arguments, body: &[Stmt]) { - if !collect_arg_names(args).contains(&"request") { + if !includes_arg_name("request", args) { return; } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs index 7fb2da8f1f..2e09517978 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/patch.rs @@ -1,10 +1,9 @@ -use rustc_hash::FxHashSet; -use rustpython_parser::ast::{self, Expr, Keyword, Ranged}; +use rustpython_parser::ast::{self, Arguments, Expr, Keyword, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; -use ruff_python_ast::helpers::{collect_arg_names, SimpleCallArgs}; +use ruff_python_ast::helpers::{includes_arg_name, SimpleCallArgs}; use ruff_python_ast::visitor; use ruff_python_ast::visitor::Visitor; @@ -18,10 +17,10 @@ impl Violation for PytestPatchWithLambda { } } -#[derive(Default)] /// Visitor that checks references the argument names in the lambda body. +#[derive(Debug)] struct LambdaBodyVisitor<'a> { - names: FxHashSet<&'a str>, + arguments: &'a Arguments, uses_args: bool, } @@ -32,11 +31,15 @@ where fn visit_expr(&mut self, expr: &'b Expr) { match expr { Expr::Name(ast::ExprName { id, .. }) => { - if self.names.contains(&id.as_str()) { + if includes_arg_name(id, self.arguments) { self.uses_args = true; } } - _ => visitor::walk_expr(self, expr), + _ => { + if !self.uses_args { + visitor::walk_expr(self, expr); + } + } } } } @@ -60,7 +63,7 @@ fn check_patch_call( { // Walk the lambda body. let mut visitor = LambdaBodyVisitor { - names: collect_arg_names(args), + arguments: args, uses_args: false, }; visitor.visit_expr(body); diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 486f7b29ad..596783db47 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -4,7 +4,7 @@ use std::path::Path; use num_traits::Zero; use ruff_text_size::{TextRange, TextSize}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use rustpython_ast::CmpOp; use rustpython_parser::ast::{ self, Arguments, Constant, ExceptHandler, Expr, Keyword, MatchCase, Pattern, Ranged, Stmt, @@ -669,25 +669,28 @@ pub fn extract_handled_exceptions(handlers: &[ExceptHandler]) -> Vec<&Expr> { handled_exceptions } -/// Return the set of all bound argument names. -pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> { - let mut arg_names: FxHashSet<&'a str> = FxHashSet::default(); - for arg_with_default in &arguments.posonlyargs { - arg_names.insert(arg_with_default.def.arg.as_str()); - } - for arg_with_default in &arguments.args { - arg_names.insert(arg_with_default.def.arg.as_str()); +/// Returns `true` if the given name is included in the given [`Arguments`]. +pub fn includes_arg_name(name: &str, arguments: &Arguments) -> bool { + if arguments + .posonlyargs + .iter() + .chain(&arguments.args) + .chain(&arguments.kwonlyargs) + .any(|arg| arg.def.arg.as_str() == name) + { + return true; } if let Some(arg) = &arguments.vararg { - arg_names.insert(arg.arg.as_str()); - } - for arg_with_default in &arguments.kwonlyargs { - arg_names.insert(arg_with_default.def.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } if let Some(arg) = &arguments.kwarg { - arg_names.insert(arg.arg.as_str()); + if arg.arg.as_str() == name { + return true; + } } - arg_names + false } /// Given an [`Expr`] that can be callable or not (like a decorator, which could From 00fbbe4223d0cbfc3014403ace659f48368e1852 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 12:29:59 -0400 Subject: [PATCH 16/27] Remove some additional manual iterator matches (#5482) ## Summary I've done a few of these PRs, I thought I'd caught them all, but missed this pattern. --- crates/ruff/src/checkers/ast/mod.rs | 5 +- .../rules/request_with_no_cert_validation.rs | 30 +++--------- .../rules/nullable_model_string_field.rs | 20 +++----- .../src/analyze/function_type.rs | 35 +++++++------- crates/ruff_python_stdlib/src/identifiers.rs | 8 ++-- crates/ruff_python_stdlib/src/keyword.rs | 46 ++++++++++++++++--- 6 files changed, 75 insertions(+), 69 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 0d8a631558..7abc449cdb 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -644,10 +644,7 @@ where }, ) => { if self.enabled(Rule::DjangoNullableModelStringField) { - self.diagnostics - .extend(flake8_django::rules::nullable_model_string_field( - self, body, - )); + flake8_django::rules::nullable_model_string_field(self, body); } if self.enabled(Rule::DjangoExcludeWithModelForm) { if let Some(diagnostic) = diff --git a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs index b03020e7eb..27c22af441 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/request_with_no_cert_validation.rs @@ -21,21 +21,6 @@ impl Violation for RequestWithNoCertValidation { } } -const REQUESTS_HTTP_VERBS: [&str; 7] = ["get", "options", "head", "post", "put", "patch", "delete"]; -const HTTPX_METHODS: [&str; 11] = [ - "get", - "options", - "head", - "post", - "put", - "patch", - "delete", - "request", - "stream", - "Client", - "AsyncClient", -]; - /// S501 pub(crate) fn request_with_no_cert_validation( checker: &mut Checker, @@ -46,16 +31,13 @@ pub(crate) fn request_with_no_cert_validation( if let Some(target) = checker .semantic() .resolve_call_path(func) - .and_then(|call_path| { - if call_path.len() == 2 { - if call_path[0] == "requests" && REQUESTS_HTTP_VERBS.contains(&call_path[1]) { - return Some("requests"); - } - if call_path[0] == "httpx" && HTTPX_METHODS.contains(&call_path[1]) { - return Some("httpx"); - } + .and_then(|call_path| match call_path.as_slice() { + ["requests", "get" | "options" | "head" | "post" | "put" | "patch" | "delete"] => { + Some("requests") } - None + ["httpx", "get" | "options" | "head" | "post" | "put" | "patch" | "delete" | "request" + | "stream" | "Client" | "AsyncClient"] => Some("httpx"), + _ => None, }) { let call_args = SimpleCallArgs::new(args, keywords); diff --git a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs index 6da8d4ba50..b74bf19a8c 100644 --- a/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs +++ b/crates/ruff/src/rules/flake8_django/rules/nullable_model_string_field.rs @@ -51,24 +51,14 @@ impl Violation for DjangoNullableModelStringField { } } -const NOT_NULL_TRUE_FIELDS: [&str; 6] = [ - "CharField", - "TextField", - "SlugField", - "EmailField", - "FilePathField", - "URLField", -]; - /// DJ001 -pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> Vec { - let mut errors = Vec::new(); +pub(crate) fn nullable_model_string_field(checker: &mut Checker, body: &[Stmt]) { for statement in body.iter() { let Stmt::Assign(ast::StmtAssign { value, .. }) = statement else { continue; }; if let Some(field_name) = is_nullable_field(checker, value) { - errors.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNullableModelStringField { field_name: field_name.to_string(), }, @@ -76,7 +66,6 @@ pub(crate) fn nullable_model_string_field(checker: &Checker, body: &[Stmt]) -> V )); } } - errors } fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> { @@ -88,7 +77,10 @@ fn is_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a st return None; }; - if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) { + if !matches!( + valid_field_name, + "CharField" | "TextField" | "SlugField" | "EmailField" | "FilePathField" | "URLField" + ) { return None; } diff --git a/crates/ruff_python_semantic/src/analyze/function_type.rs b/crates/ruff_python_semantic/src/analyze/function_type.rs index 63f9b8ce71..95b0f3845e 100644 --- a/crates/ruff_python_semantic/src/analyze/function_type.rs +++ b/crates/ruff_python_semantic/src/analyze/function_type.rs @@ -6,10 +6,7 @@ use ruff_python_ast::helpers::map_callable; use crate::model::SemanticModel; use crate::scope::{Scope, ScopeKind}; -const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"]; -const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")]; - -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum FunctionType { Function, Method, @@ -44,24 +41,28 @@ pub fn classify( }) }) { FunctionType::StaticMethod - } else if CLASS_METHODS.contains(&name) - // Special-case class method, like `__new__`. + } else if matches!(name, "__new__" | "__init_subclass__" | "__class_getitem__") + // Special-case class method, like `__new__`. || scope.bases.iter().any(|expr| { // The class itself extends a known metaclass, so all methods are class methods. - semantic.resolve_call_path(map_callable(expr)).map_or(false, |call_path| { - METACLASS_BASES - .iter() - .any(|(module, member)| call_path.as_slice() == [*module, *member]) - }) + semantic + .resolve_call_path(map_callable(expr)) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["", "type"] | ["abc", "ABCMeta"]) + }) }) || decorator_list.iter().any(|decorator| { // The method is decorated with a class method decorator (like `@classmethod`). - semantic.resolve_call_path(map_callable(&decorator.expression)).map_or(false, |call_path| { - matches!(call_path.as_slice(), ["", "classmethod"] | ["abc", "abstractclassmethod"]) || - classmethod_decorators - .iter() - .any(|decorator| call_path == from_qualified_name(decorator)) - }) + semantic + .resolve_call_path(map_callable(&decorator.expression)) + .map_or(false, |call_path| { + matches!( + call_path.as_slice(), + ["", "classmethod"] | ["abc", "abstractclassmethod"] + ) || classmethod_decorators + .iter() + .any(|decorator| call_path == from_qualified_name(decorator)) + }) }) { FunctionType::ClassMethod diff --git a/crates/ruff_python_stdlib/src/identifiers.rs b/crates/ruff_python_stdlib/src/identifiers.rs index 2dca484a3c..169959bee2 100644 --- a/crates/ruff_python_stdlib/src/identifiers.rs +++ b/crates/ruff_python_stdlib/src/identifiers.rs @@ -1,4 +1,4 @@ -use crate::keyword::KWLIST; +use crate::keyword::is_keyword; /// Returns `true` if a string is a valid Python identifier (e.g., variable /// name). @@ -18,7 +18,7 @@ pub fn is_identifier(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -52,7 +52,7 @@ pub fn is_module_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } @@ -70,7 +70,7 @@ pub fn is_migration_name(name: &str) -> bool { } // Is the identifier a keyword? - if KWLIST.contains(&name) { + if is_keyword(name) { return false; } diff --git a/crates/ruff_python_stdlib/src/keyword.rs b/crates/ruff_python_stdlib/src/keyword.rs index ddaec03c80..7f361c0b69 100644 --- a/crates/ruff_python_stdlib/src/keyword.rs +++ b/crates/ruff_python_stdlib/src/keyword.rs @@ -1,7 +1,41 @@ // See: https://github.com/python/cpython/blob/9d692841691590c25e6cf5b2250a594d3bf54825/Lib/keyword.py#L18 -pub(crate) const KWLIST: [&str; 35] = [ - "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", - "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", - "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", - "with", "yield", -]; +pub(crate) fn is_keyword(name: &str) -> bool { + matches!( + name, + "False" + | "None" + | "True" + | "and" + | "as" + | "assert" + | "async" + | "await" + | "break" + | "class" + | "continue" + | "def" + | "del" + | "elif" + | "else" + | "except" + | "finally" + | "for" + | "from" + | "global" + | "if" + | "import" + | "in" + | "is" + | "lambda" + | "nonlocal" + | "not" + | "or" + | "pass" + | "raise" + | "return" + | "try" + | "while" + | "with" + | "yield", + ) +} From ca497fabbd970a557ed1b45acccaa714e12573af Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 12:47:23 -0400 Subject: [PATCH 17/27] Remove some `diagnostics.extend` calls (#5483) ## Summary It's more efficient (and more idiomatic for us) to pass in the `Checker` directly. --- crates/ruff/src/checkers/ast/mod.rs | 40 ++++++------------- .../rules/hardcoded_password_default.rs | 9 ++--- .../rules/hardcoded_password_func_arg.rs | 12 +++--- .../rules/hardcoded_password_string.rs | 21 +++++----- .../rules/non_leading_receiver_decorator.rs | 28 +++++-------- .../rules/f_string_in_gettext_func_call.rs | 9 +++-- .../rules/format_in_gettext_func_call.rs | 9 +++-- .../rules/printf_in_gettext_func_call.rs | 8 ++-- .../flake8_pytest_style/rules/assertion.rs | 11 +++-- .../ruff/src/rules/flake8_return/visitor.rs | 18 ++++----- 10 files changed, 75 insertions(+), 90 deletions(-) diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 7abc449cdb..257dcdcf17 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -359,11 +359,7 @@ where .. }) => { if self.enabled(Rule::DjangoNonLeadingReceiverDecorator) { - self.diagnostics - .extend(flake8_django::rules::non_leading_receiver_decorator( - decorator_list, - |expr| self.semantic.resolve_call_path(expr), - )); + flake8_django::rules::non_leading_receiver_decorator(self, decorator_list); } if self.enabled(Rule::AmbiguousFunctionName) { if let Some(diagnostic) = @@ -505,8 +501,7 @@ where } } if self.enabled(Rule::HardcodedPasswordDefault) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_default(args)); + flake8_bandit::rules::hardcoded_password_default(self, args); } if self.enabled(Rule::PropertyWithParameters) { pylint::rules::property_with_parameters(self, stmt, decorator_list, args); @@ -1573,9 +1568,7 @@ where pyupgrade::rules::os_error_alias_handlers(self, handlers); } if self.enabled(Rule::PytestAssertInExcept) { - self.diagnostics.extend( - flake8_pytest_style::rules::assert_in_exception_handler(handlers), - ); + flake8_pytest_style::rules::assert_in_exception_handler(self, handlers); } if self.enabled(Rule::SuppressibleException) { flake8_simplify::rules::suppressible_exception( @@ -1616,11 +1609,7 @@ where flake8_bugbear::rules::assignment_to_os_environ(self, targets); } if self.enabled(Rule::HardcodedPasswordString) { - if let Some(diagnostic) = - flake8_bandit::rules::assign_hardcoded_password_string(value, targets) - { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::assign_hardcoded_password_string(self, value, targets); } if self.enabled(Rule::GlobalStatement) { for target in targets.iter() { @@ -2615,8 +2604,7 @@ where flake8_bandit::rules::jinja2_autoescape_false(self, func, args, keywords); } if self.enabled(Rule::HardcodedPasswordFuncArg) { - self.diagnostics - .extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords)); + flake8_bandit::rules::hardcoded_password_func_arg(self, keywords); } if self.enabled(Rule::HardcodedSQLExpression) { flake8_bandit::rules::hardcoded_sql_expression(self, expr); @@ -2871,16 +2859,13 @@ where &self.settings.flake8_gettext.functions_names, ) { if self.enabled(Rule::FStringInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::f_string_in_gettext_func_call(args)); + flake8_gettext::rules::f_string_in_gettext_func_call(self, args); } if self.enabled(Rule::FormatInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::format_in_gettext_func_call(args)); + flake8_gettext::rules::format_in_gettext_func_call(self, args); } if self.enabled(Rule::PrintfInGetTextFuncCall) { - self.diagnostics - .extend(flake8_gettext::rules::printf_in_gettext_func_call(args)); + flake8_gettext::rules::printf_in_gettext_func_call(self, args); } } if self.enabled(Rule::UncapitalizedEnvironmentVariables) { @@ -3221,11 +3206,10 @@ where flake8_2020::rules::compare(self, left, ops, comparators); } if self.enabled(Rule::HardcodedPasswordString) { - self.diagnostics.extend( - flake8_bandit::rules::compare_to_hardcoded_password_string( - left, - comparators, - ), + flake8_bandit::rules::compare_to_hardcoded_password_string( + self, + left, + comparators, ); } if self.enabled(Rule::ComparisonWithItself) { diff --git a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs index 127ce31199..50be800d9e 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/hardcoded_password_default.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{Arg, ArgWithDefault, Arguments, Expr, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -36,9 +37,7 @@ fn check_password_kwarg(arg: &Arg, default: &Expr) -> Option { } /// S107 -pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec { - let mut diagnostics: Vec = Vec::new(); - +pub(crate) fn hardcoded_password_default(checker: &mut Checker, arguments: &Arguments) { for ArgWithDefault { def, default, @@ -53,9 +52,7 @@ pub(crate) fn hardcoded_password_default(arguments: &Arguments) -> Vec Vec { - keywords - .iter() - .filter_map(|keyword| { +pub(crate) fn hardcoded_password_func_arg(checker: &mut Checker, keywords: &[Keyword]) { + checker + .diagnostics + .extend(keywords.iter().filter_map(|keyword| { string_literal(&keyword.value).filter(|string| !string.is_empty())?; let arg = keyword.arg.as_ref()?; if !matches_password_name(arg) { @@ -37,6 +38,5 @@ pub(crate) fn hardcoded_password_func_arg(keywords: &[Keyword]) -> Vec Option<&str> { /// S105 pub(crate) fn compare_to_hardcoded_password_string( + checker: &mut Checker, left: &Expr, comparators: &[Expr], -) -> Vec { - comparators - .iter() - .filter_map(|comp| { +) { + checker + .diagnostics + .extend(comparators.iter().filter_map(|comp| { string_literal(comp).filter(|string| !string.is_empty())?; let Some(name) = password_target(left) else { return None; @@ -63,29 +66,29 @@ pub(crate) fn compare_to_hardcoded_password_string( }, comp.range(), )) - }) - .collect() + })); } /// S105 pub(crate) fn assign_hardcoded_password_string( + checker: &mut Checker, value: &Expr, targets: &[Expr], -) -> Option { +) { if string_literal(value) .filter(|string| !string.is_empty()) .is_some() { for target in targets { if let Some(name) = password_target(target) { - return Some(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( HardcodedPasswordString { name: name.to_string(), }, value.range(), )); + return; } } } - None } diff --git a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs index f023431404..9e823858c0 100644 --- a/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs +++ b/crates/ruff/src/rules/flake8_django/rules/non_leading_receiver_decorator.rs @@ -1,8 +1,9 @@ -use rustpython_parser::ast::{self, Decorator, Expr, Ranged}; +use rustpython_parser::ast::{Decorator, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::call_path::CallPath; + +use crate::checkers::ast::Checker; /// ## What it does /// Checks that Django's `@receiver` decorator is listed first, prior to @@ -48,25 +49,19 @@ impl Violation for DjangoNonLeadingReceiverDecorator { } /// DJ013 -pub(crate) fn non_leading_receiver_decorator<'a, F>( - decorator_list: &'a [Decorator], - resolve_call_path: F, -) -> Vec -where - F: Fn(&'a Expr) -> Option>, -{ - let mut diagnostics = vec![]; +pub(crate) fn non_leading_receiver_decorator(checker: &mut Checker, decorator_list: &[Decorator]) { let mut seen_receiver = false; for (i, decorator) in decorator_list.iter().enumerate() { - let is_receiver = match &decorator.expression { - Expr::Call(ast::ExprCall { func, .. }) => resolve_call_path(func) + let is_receiver = decorator.expression.as_call_expr().map_or(false, |call| { + checker + .semantic() + .resolve_call_path(&call.func) .map_or(false, |call_path| { matches!(call_path.as_slice(), ["django", "dispatch", "receiver"]) - }), - _ => false, - }; + }) + }); if i > 0 && is_receiver && !seen_receiver { - diagnostics.push(Diagnostic::new( + checker.diagnostics.push(Diagnostic::new( DjangoNonLeadingReceiverDecorator, decorator.range(), )); @@ -77,5 +72,4 @@ where seen_receiver = true; } } - diagnostics } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs index 0f6b8c6a60..89957a02ac 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/f_string_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FStringInGetTextFuncCall; @@ -14,11 +16,12 @@ impl Violation for FStringInGetTextFuncCall { } /// INT001 -pub(crate) fn f_string_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if first.is_joined_str_expr() { - return Some(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range())); } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs index ec159d0337..2f99369f25 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/format_in_gettext_func_call.rs @@ -3,6 +3,8 @@ use rustpython_parser::ast::{self, Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct FormatInGetTextFuncCall; @@ -14,15 +16,16 @@ impl Violation for FormatInGetTextFuncCall { } /// INT002 -pub(crate) fn format_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn format_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::Call(ast::ExprCall { func, .. }) = &first { if let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() { if attr == "format" { - return Some(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(FormatInGetTextFuncCall {}, first.range())); } } } } - None } diff --git a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs index 088eaa60f8..eab5d74c93 100644 --- a/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs +++ b/crates/ruff/src/rules/flake8_gettext/rules/printf_in_gettext_func_call.rs @@ -1,5 +1,6 @@ use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use crate::checkers::ast::Checker; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; @@ -14,7 +15,7 @@ impl Violation for PrintfInGetTextFuncCall { } /// INT003 -pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { +pub(crate) fn printf_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) { if let Some(first) = args.first() { if let Expr::BinOp(ast::ExprBinOp { op: Operator::Mod { .. }, @@ -27,9 +28,10 @@ pub(crate) fn printf_in_gettext_func_call(args: &[Expr]) -> Option { .. }) = left.as_ref() { - return Some(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); + checker + .diagnostics + .push(Diagnostic::new(PrintfInGetTextFuncCall {}, first.range())); } } } - None } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index b4e5aa23be..363b327c8c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -226,10 +226,10 @@ pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { } /// PT017 -pub(crate) fn assert_in_exception_handler(handlers: &[ExceptHandler]) -> Vec { - handlers - .iter() - .flat_map(|handler| match handler { +pub(crate) fn assert_in_exception_handler(checker: &mut Checker, handlers: &[ExceptHandler]) { + checker + .diagnostics + .extend(handlers.iter().flat_map(|handler| match handler { ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler { name, body, .. }) => { @@ -239,8 +239,7 @@ pub(crate) fn assert_in_exception_handler(handlers: &[ExceptHandler]) -> Vec { +pub(super) struct Stack<'a> { /// The `return` statements in the current function. - pub(crate) returns: Vec<&'a ast::StmtReturn>, + pub(super) returns: Vec<&'a ast::StmtReturn>, /// The `else` statements in the current function. - pub(crate) elses: Vec<&'a ast::StmtIf>, + pub(super) elses: Vec<&'a ast::StmtIf>, /// The `elif` statements in the current function. - pub(crate) elifs: Vec<&'a ast::StmtIf>, + pub(super) elifs: Vec<&'a ast::StmtIf>, /// The non-local variables in the current function. - pub(crate) non_locals: FxHashSet<&'a str>, + pub(super) non_locals: FxHashSet<&'a str>, /// Whether the current function is a generator. - pub(crate) is_generator: bool, + pub(super) is_generator: bool, /// The `assignment`-to-`return` statement pairs in the current function. /// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement /// removal for the `return` statement. - pub(crate) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, + pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>, } #[derive(Default)] -pub(crate) struct ReturnVisitor<'a> { +pub(super) struct ReturnVisitor<'a> { /// The current stack of nodes. - pub(crate) stack: Stack<'a>, + pub(super) stack: Stack<'a>, /// The preceding sibling of the current node. sibling: Option<&'a Stmt>, /// The parent nodes of the current node. From ed1dd09d02af7972df301dac0c6b5d084f26cc1b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 13:53:17 -0400 Subject: [PATCH 18/27] Refine some `perflint` rules (#5484) ## Summary Removing some false positives based on running over `zulip`. `PERF401` now also detects cases like: ```py original = list(range(10000)) filtered = [] for i in original: filtered.append(i * i) ``` Previously, these were caught by the list-copy rule, but these too need comprehensions. --- .../test/fixtures/perflint/PERF401.py | 20 ++++- .../test/fixtures/perflint/PERF402.py | 11 ++- crates/ruff/src/checkers/ast/mod.rs | 4 +- .../rules/manual_list_comprehension.rs | 84 ++++++++++++++----- .../rules/perflint/rules/manual_list_copy.rs | 27 +++++- ...__perflint__tests__PERF401_PERF401.py.snap | 12 ++- ...__perflint__tests__PERF402_PERF402.py.snap | 8 -- 7 files changed, 122 insertions(+), 44 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF401.py b/crates/ruff/resources/test/fixtures/perflint/PERF401.py index ac19d19876..beb3d4546c 100644 --- a/crates/ruff/resources/test/fixtures/perflint/PERF401.py +++ b/crates/ruff/resources/test/fixtures/perflint/PERF401.py @@ -1,4 +1,4 @@ -def foo(): +def f(): items = [1, 2, 3, 4] result = [] for i in items: @@ -6,8 +6,15 @@ def foo(): result.append(i) # PERF401 -def foo(): - items = [1,2,3,4] +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i * i) # PERF401 + + +def f(): + items = [1, 2, 3, 4] result = [] for i in items: if i % 2: @@ -16,3 +23,10 @@ def foo(): result.append(i) # PERF401 else: result.append(i) # PERF401 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i) # OK diff --git a/crates/ruff/resources/test/fixtures/perflint/PERF402.py b/crates/ruff/resources/test/fixtures/perflint/PERF402.py index 0d6842dce7..4db9a3dc52 100644 --- a/crates/ruff/resources/test/fixtures/perflint/PERF402.py +++ b/crates/ruff/resources/test/fixtures/perflint/PERF402.py @@ -1,12 +1,19 @@ -def foo(): +def f(): items = [1, 2, 3, 4] result = [] for i in items: result.append(i) # PERF402 -def foo(): +def f(): items = [1, 2, 3, 4] result = [] for i in items: result.insert(0, i) # PERF402 + + +def f(): + items = [1, 2, 3, 4] + result = [] + for i in items: + result.append(i * i) # OK diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 257dcdcf17..6eb1f6b25c 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1525,10 +1525,10 @@ where perflint::rules::incorrect_dict_iterator(self, target, iter); } if self.enabled(Rule::ManualListComprehension) { - perflint::rules::manual_list_comprehension(self, body); + perflint::rules::manual_list_comprehension(self, target, body); } if self.enabled(Rule::ManualListCopy) { - perflint::rules::manual_list_copy(self, body); + perflint::rules::manual_list_copy(self, target, body); } if self.enabled(Rule::UnnecessaryListCast) { perflint::rules::unnecessary_list_cast(self, iter); diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs index d7143bb080..eb6e56735b 100644 --- a/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs +++ b/crates/ruff/src/rules/perflint/rules/manual_list_comprehension.rs @@ -9,7 +9,7 @@ use crate::checkers::ast::Checker; /// Checks for `for` loops that can be replaced by a list comprehension. /// /// ## Why is this bad? -/// When creating a filtered list from an existing list using a for-loop, +/// When creating a transformed list from an existing list using a for-loop, /// prefer a list comprehension. List comprehensions are more readable and /// more performant. /// @@ -34,43 +34,87 @@ use crate::checkers::ast::Checker; /// original = list(range(10000)) /// filtered = [x for x in original if x % 2] /// ``` +/// +/// If you're appending to an existing list, use the `extend` method instead: +/// ```python +/// original = list(range(10000)) +/// filtered.extend(x for x in original if x % 2) +/// ``` #[violation] pub struct ManualListComprehension; impl Violation for ManualListComprehension { #[derive_message_formats] fn message(&self) -> String { - format!("Use a list comprehension to create a new filtered list") + format!("Use a list comprehension to create a transformed list") } } /// PERF401 -pub(crate) fn manual_list_comprehension(checker: &mut Checker, body: &[Stmt]) { - let [stmt] = body else { +pub(crate) fn manual_list_comprehension(checker: &mut Checker, target: &Expr, body: &[Stmt]) { + let (stmt, conditional) = match body { + // ```python + // for x in y: + // if z: + // filtered.append(x) + // ``` + [Stmt::If(ast::StmtIf { body, orelse, .. })] => { + if !orelse.is_empty() { + return; + } + let [stmt] = body.as_slice() else { + return; + }; + (stmt, true) + } + // ```python + // for x in y: + // filtered.append(f(x)) + // ``` + [stmt] => (stmt, false), + _ => return, + }; + + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { return; }; - let Stmt::If(ast::StmtIf { body, .. }) = stmt else { + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { return; }; - for stmt in body { - let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { - continue; - }; + if !keywords.is_empty() { + return; + } - let Expr::Call(ast::ExprCall { func, range, .. }) = value.as_ref() else { - continue; - }; + let [arg] = args.as_slice() else { + return; + }; - let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { - continue; - }; - - if attr.as_str() == "append" { - checker - .diagnostics - .push(Diagnostic::new(ManualListComprehension, *range)); + // Ignore direct list copies (e.g., `for x in y: filtered.append(x)`). + if !conditional { + if arg.as_name_expr().map_or(false, |arg| { + target + .as_name_expr() + .map_or(false, |target| arg.id == target.id) + }) { + return; } } + + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { + return; + }; + + if attr.as_str() == "append" { + checker + .diagnostics + .push(Diagnostic::new(ManualListComprehension, *range)); + } } diff --git a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs index dd7545129d..13554488bd 100644 --- a/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs +++ b/crates/ruff/src/rules/perflint/rules/manual_list_copy.rs @@ -44,7 +44,7 @@ impl Violation for ManualListCopy { } /// PERF402 -pub(crate) fn manual_list_copy(checker: &mut Checker, body: &[Stmt]) { +pub(crate) fn manual_list_copy(checker: &mut Checker, target: &Expr, body: &[Stmt]) { let [stmt] = body else { return; }; @@ -53,10 +53,33 @@ pub(crate) fn manual_list_copy(checker: &mut Checker, body: &[Stmt]) { return; }; - let Expr::Call(ast::ExprCall { func, range, .. }) = value.as_ref() else { + let Expr::Call(ast::ExprCall { + func, + range, + args, + keywords, + }) = value.as_ref() + else { return; }; + if !keywords.is_empty() { + return; + } + + let [arg] = args.as_slice() else { + return; + }; + + // Only flag direct list copies (e.g., `for x in y: filtered.append(x)`). + if !arg.as_name_expr().map_or(false, |arg| { + target + .as_name_expr() + .map_or(false, |target| arg.id == target.id) + }) { + return; + } + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func.as_ref() else { return; }; diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap index e59eae4adc..cf2e2677c5 100644 --- a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF401_PERF401.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/perflint/mod.rs --- -PERF401.py:6:13: PERF401 Use a list comprehension to create a new filtered list +PERF401.py:6:13: PERF401 Use a list comprehension to create a transformed list | 4 | for i in items: 5 | if i % 2: @@ -9,14 +9,12 @@ PERF401.py:6:13: PERF401 Use a list comprehension to create a new filtered list | ^^^^^^^^^^^^^^^^ PERF401 | -PERF401.py:14:13: PERF401 Use a list comprehension to create a new filtered list +PERF401.py:13:9: PERF401 Use a list comprehension to create a transformed list | +11 | result = [] 12 | for i in items: -13 | if i % 2: -14 | result.append(i) # PERF401 - | ^^^^^^^^^^^^^^^^ PERF401 -15 | elif i % 2: -16 | result.append(i) # PERF401 +13 | result.append(i * i) # PERF401 + | ^^^^^^^^^^^^^^^^^^^^ PERF401 | diff --git a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap index 55cd69db8b..e56584c95e 100644 --- a/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap +++ b/crates/ruff/src/rules/perflint/snapshots/ruff__rules__perflint__tests__PERF402_PERF402.py.snap @@ -9,12 +9,4 @@ PERF402.py:5:9: PERF402 Use `list` or `list.copy` to create a copy of a list | ^^^^^^^^^^^^^^^^ PERF402 | -PERF402.py:12:9: PERF402 Use `list` or `list.copy` to create a copy of a list - | -10 | result = [] -11 | for i in items: -12 | result.insert(0, i) # PERF402 - | ^^^^^^^^^^^^^^^^^^^ PERF402 - | - From 8de5a3d29df5964dd91f39d0150caccc7bf0ab83 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 13:57:49 -0400 Subject: [PATCH 19/27] Allow `Final` assignments in stubs (#5490) ## Summary This fixes one incompatibility with `flake8-pyi`, and gives us a clean pass on `typeshed`. --- .../resources/test/fixtures/flake8_pyi/PYI015.py | 1 + .../resources/test/fixtures/flake8_pyi/PYI015.pyi | 1 + .../src/rules/flake8_pyi/rules/simple_defaults.rs | 13 +++++++++++++ 3 files changed, 15 insertions(+) diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py index 37a4f4d867..16ca34e318 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.py @@ -91,3 +91,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi index 860ee255fb..10f7a70770 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI015.pyi @@ -98,3 +98,4 @@ field27 = list[str] field28 = builtins.str field29 = str field30 = str | bytes | None +field31: typing.Final = field30 diff --git a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs index ec891299d9..b1ed3595b0 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -298,6 +298,16 @@ fn is_special_assignment(target: &Expr, semantic: &SemanticModel) -> bool { } } +/// Returns `true` if this is an assignment to a simple `Final`-annotated variable. +fn is_final_assignment(annotation: &Expr, value: &Expr, semantic: &SemanticModel) -> bool { + if matches!(value, Expr::Name(_) | Expr::Attribute(_)) { + if semantic.match_typing_expr(annotation, "Final") { + return true; + } + } + false +} + /// Returns `true` if the a class is an enum, based on its base classes. fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool { return bases.iter().any(|expr| { @@ -438,6 +448,9 @@ pub(crate) fn annotated_assignment_default_in_stub( if is_type_var_like_call(value, checker.semantic()) { return; } + if is_final_assignment(annotation, value, checker.semantic()) { + return; + } if is_valid_default_value_with_annotation(value, true, checker.locator, checker.semantic()) { return; } From 3992c47c008df8f706e03a6ba0d7aa7f068ef0a9 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 3 Jul 2023 14:02:49 -0400 Subject: [PATCH 20/27] Bump version to 0.0.276 (#5488) --- Cargo.lock | 6 +++--- README.md | 2 +- crates/flake8_to_ruff/Cargo.toml | 2 +- crates/ruff/Cargo.toml | 2 +- crates/ruff_cli/Cargo.toml | 2 +- docs/tutorial.md | 2 +- docs/usage.md | 4 ++-- pyproject.toml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e7fbc656e..ca1ef09290 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "flake8-to-ruff" -version = "0.0.275" +version = "0.0.276" dependencies = [ "anyhow", "clap", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "ruff" -version = "0.0.275" +version = "0.0.276" dependencies = [ "annotate-snippets 0.9.1", "anyhow", @@ -1926,7 +1926,7 @@ dependencies = [ [[package]] name = "ruff_cli" -version = "0.0.275" +version = "0.0.276" dependencies = [ "annotate-snippets 0.9.1", "anyhow", diff --git a/README.md b/README.md index 46749a3e84..1a73e65b38 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` diff --git a/crates/flake8_to_ruff/Cargo.toml b/crates/flake8_to_ruff/Cargo.toml index f8191279e2..c411d33fe6 100644 --- a/crates/flake8_to_ruff/Cargo.toml +++ b/crates/flake8_to_ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flake8-to-ruff" -version = "0.0.275" +version = "0.0.276" description = """ Convert Flake8 configuration files to Ruff configuration files. """ diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 76b09f6474..350af4bebc 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff" -version = "0.0.275" +version = "0.0.276" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/crates/ruff_cli/Cargo.toml b/crates/ruff_cli/Cargo.toml index 75fa5a9a11..212c59de10 100644 --- a/crates/ruff_cli/Cargo.toml +++ b/crates/ruff_cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ruff_cli" -version = "0.0.275" +version = "0.0.276" publish = false authors = { workspace = true } edition = { workspace = true } diff --git a/docs/tutorial.md b/docs/tutorial.md index e9f6592061..452bb2583e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -242,7 +242,7 @@ This tutorial has focused on Ruff's command-line interface, but Ruff can also be ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` diff --git a/docs/usage.md b/docs/usage.md index 8528ec9142..4d2f32448f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -22,7 +22,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff ``` @@ -32,7 +32,7 @@ Or, to enable autofix: ```yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.275 + rev: v0.0.276 hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] diff --git a/pyproject.toml b/pyproject.toml index e5e55e84ef..b860e31629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "ruff" -version = "0.0.275" +version = "0.0.276" description = "An extremely fast Python linter, written in Rust." authors = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] maintainers = [{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" }] From a647f31600532fdc8fca65f2b0e5be4f1de68b98 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 3 Jul 2023 21:48:44 +0200 Subject: [PATCH 21/27] Don't add a magic trailing comma for a single entry (#5463) ## Summary If a comma separated list has only one entry, black will respect the magic trailing comma, but it will not add a new one. The following code will remain as is: ```python b1 = [ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ] b2 = [ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, ] b3 = [ aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa ] ``` ## Test Plan This was first discovered in https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681, which i've minimized into a call test. I've added tests for the three cases (one entry + no comma, one entry + comma, more than one entry) to the list tests. The diffs from the black tests get smaller. --- .../ruff/{statement => expression}/call.py | 5 ++++ .../test/fixtures/ruff/expression/list.py | 13 ++++++++ crates/ruff_python_formatter/src/builders.rs | 30 ++++++++++++------- ...aneous__long_strings_flag_disabled.py.snap | 29 +++++------------- ...ity@py_310__pattern_matching_style.py.snap | 12 ++++---- ...patibility@simple_cases__comments3.py.snap | 11 +------ ...tibility@simple_cases__composition.py.snap | 11 +------ ...ses__composition_no_trailing_comma.py.snap | 11 +------ ...mpatibility@simple_cases__fmtonoff.py.snap | 10 ++----- ...patibility@simple_cases__fmtonoff4.py.snap | 15 ++-------- ...patibility@simple_cases__fmtonoff5.py.snap | 4 +-- ...mpatibility@simple_cases__function.py.snap | 28 ++++------------- ...ple_cases__function_trailing_comma.py.snap | 30 ++++--------------- .../format@expression__binary.py.snap | 4 +-- ...y.snap => format@expression__call.py.snap} | 17 +++++++++-- .../format@expression__compare.py.snap | 4 +-- .../snapshots/format@expression__dict.py.snap | 4 +-- .../snapshots/format@expression__list.py.snap | 26 ++++++++++++++++ .../snapshots/format@statement__with.py.snap | 4 +-- 19 files changed, 119 insertions(+), 149 deletions(-) rename crates/ruff_python_formatter/resources/test/fixtures/ruff/{statement => expression}/call.py (85%) rename crates/ruff_python_formatter/tests/snapshots/{format@statement__call.py.snap => format@expression__call.py.snap} (83%) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py similarity index 85% rename from crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py rename to crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py index 7a32b6cd28..8c372180ce 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py @@ -81,3 +81,8 @@ f( dict() ) +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py index 2ec1c0e293..f0fedc6957 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/list.py @@ -8,3 +8,16 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 97a70dbe47..4541acda74 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -182,7 +182,10 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { pub(crate) struct JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { result: FormatResult<()>, fmt: &'fmt mut PyFormatter<'ast, 'buf>, - last_end: Option, + end_of_last_entry: Option, + /// We need to track whether we have more than one entry since a sole entry doesn't get a + /// magic trailing comma even when expanded + len: usize, } impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { @@ -190,7 +193,8 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { Self { fmt: f, result: Ok(()), - last_end: None, + end_of_last_entry: None, + len: 0, } } @@ -203,11 +207,12 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { T: Ranged, { self.result = self.result.and_then(|_| { - if self.last_end.is_some() { + if self.end_of_last_entry.is_some() { write!(self.fmt, [text(","), soft_line_break_or_space()])?; } - self.last_end = Some(node.end()); + self.end_of_last_entry = Some(node.end()); + self.len += 1; content.fmt(self.fmt) }); @@ -243,18 +248,23 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn finish(&mut self) -> FormatResult<()> { self.result.and_then(|_| { - if let Some(last_end) = self.last_end.take() { - if_group_breaks(&text(",")).fmt(self.fmt)?; - - if self.fmt.options().magic_trailing_comma().is_respect() + if let Some(last_end) = self.end_of_last_entry.take() { + let magic_trailing_comma = self.fmt.options().magic_trailing_comma().is_respect() && matches!( first_non_trivia_token(last_end, self.fmt.context().contents()), Some(Token { kind: TokenKind::Comma, .. }) - ) - { + ); + + // If there is a single entry, only keep the magic trailing comma, don't add it if + // it wasn't there. If there is more than one entry, always add it. + if magic_trailing_comma || self.len > 1 { + if_group_breaks(&text(",")).fmt(self.fmt)?; + } + + if magic_trailing_comma { expand_parent().fmt(self.fmt)?; } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap index e969e1e7a7..a3f36ae2e6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__long_strings_flag_disabled.py.snap @@ -312,15 +312,6 @@ long_unmergable_string_with_pragma = ( y = "Short string" -@@ -12,7 +12,7 @@ - ) - - print( -- "This is a really long string inside of a print statement with no extra arguments attached at the end of it." -+ "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", - ) - - D1 = { @@ -70,8 +70,8 @@ bad_split3 = ( "What if we have inline comments on " # First Comment @@ -367,7 +358,7 @@ long_unmergable_string_with_pragma = ( comment_string = "Long lines with inline comments should have their comments appended to the reformatted string's enclosing right parentheses." # This comment gets thrown to the top. -@@ -165,30 +163,18 @@ +@@ -165,25 +163,13 @@ triple_quote_string = """This is a really really really long triple quote string assignment and it should not be touched.""" @@ -397,12 +388,6 @@ long_unmergable_string_with_pragma = ( some_function_call( "With a reallly generic name and with a really really long string that is, at some point down the line, " - + added -- + " to a variable and then added to another string." -+ + " to a variable and then added to another string.", - ) - - some_function_call( @@ -212,29 +198,25 @@ ) @@ -412,7 +397,7 @@ long_unmergable_string_with_pragma = ( - " which should NOT be there." - ), + "This is a really long string argument to a function that has a trailing comma" -+ " which should NOT be there.", ++ " which should NOT be there." ) func_with_bad_comma( @@ -421,7 +406,7 @@ long_unmergable_string_with_pragma = ( - " which should NOT be there." - ), # comment after comma + "This is a really long string argument to a function that has a trailing comma" -+ " which should NOT be there.", # comment after comma ++ " which should NOT be there." # comment after comma ) func_with_bad_parens_that_wont_fit_in_one_line( @@ -498,7 +483,7 @@ print( ) print( - "This is a really long string inside of a print statement with no extra arguments attached at the end of it.", + "This is a really long string inside of a print statement with no extra arguments attached at the end of it." ) D1 = { @@ -660,7 +645,7 @@ NOT_YET_IMPLEMENTED_StmtAssert some_function_call( "With a reallly generic name and with a really really long string that is, at some point down the line, " + added - + " to a variable and then added to another string.", + + " to a variable and then added to another string." ) some_function_call( @@ -685,12 +670,12 @@ func_with_bad_comma( func_with_bad_comma( "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", + " which should NOT be there." ) func_with_bad_comma( "This is a really long string argument to a function that has a trailing comma" - " which should NOT be there.", # comment after comma + " which should NOT be there." # comment after comma ) func_with_bad_parens_that_wont_fit_in_one_line( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap index 6a8135be42..56fe93fb72 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_style.py.snap @@ -84,7 +84,7 @@ match match( -match(arg) # comment +match( -+ arg, # comment ++ arg # comment +) match() @@ -93,7 +93,7 @@ match match( -case(arg) # comment +case( -+ arg, # comment ++ arg # comment +) case() @@ -103,7 +103,7 @@ match match( -re.match(something) # fast +re.match( -+ something, # fast ++ something # fast +) re.match() -match match(): @@ -120,7 +120,7 @@ match match( NOT_YET_IMPLEMENTED_StmtMatch match( - arg, # comment + arg # comment ) match() @@ -128,7 +128,7 @@ match() match() case( - arg, # comment + arg # comment ) case() @@ -137,7 +137,7 @@ case() re.match( - something, # fast + something # fast ) re.match() NOT_YET_IMPLEMENTED_StmtMatch diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap index a79cf2b66b..2812b9d37a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments3.py.snap @@ -76,15 +76,6 @@ def func(): # Capture each of the exceptions in the MultiError along with each of their causes and contexts if isinstance(exc_value, MultiError): embedded = [] -@@ -29,7 +22,7 @@ - # copy the set of _seen exceptions so that duplicates - # shared between sub-exceptions are not omitted - _seen=set(_seen), -- ) -+ ), - # This should be left alone (after) - ) - ``` ## Ruff Output @@ -114,7 +105,7 @@ def func(): # copy the set of _seen exceptions so that duplicates # shared between sub-exceptions are not omitted _seen=set(_seen), - ), + ) # This should be left alone (after) ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap index deda69dc2e..399e4ab915 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition.py.snap @@ -203,15 +203,6 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -37,7 +37,7 @@ - batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, - ).push( - # Only send the first n items. -- items=items[:num_items] -+ items=items[:num_items], - ) - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: get_collection( @@ -418,7 +409,7 @@ class C: batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, ).push( # Only send the first n items. - items=items[:num_items], + items=items[:num_items] ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap index 993bab559a..69415159ce 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__composition_no_trailing_comma.py.snap @@ -203,15 +203,6 @@ class C: print(i) xxxxxxxxxxxxxxxx = Yyyy2YyyyyYyyyyy( push_manager=context.request.resource_manager, -@@ -37,7 +37,7 @@ - batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, - ).push( - # Only send the first n items. -- items=items[:num_items] -+ items=items[:num_items], - ) - return ( - 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' @@ -47,113 +47,46 @@ def omitting_trailers(self) -> None: get_collection( @@ -418,7 +409,7 @@ class C: batch_size=Yyyy2YyyyYyyyyYyyy.FULL_SIZE, ).push( # Only send the first n items. - items=items[:num_items], + items=items[:num_items] ) return ( 'Utterly failed doctest test for %s\n File "%s", line %s, in %s\n\n%s' diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index 4d40d39ee7..32082f2da9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -395,12 +395,8 @@ d={'a':1, # fmt: on # fmt: off # ...but comments still get reformatted even though they should not be -@@ -150,12 +172,10 @@ - ast_args.kw_defaults, - parameters, - implicit_default=True, -- ) -+ ), +@@ -153,9 +175,7 @@ + ) ) # fmt: off - a = ( @@ -610,7 +606,7 @@ def long_lines(): ast_args.kw_defaults, parameters, implicit_default=True, - ), + ) ) # fmt: off a = unnecessary_bracket() diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap index 89b2436af1..a8dc2ef620 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff4.py.snap @@ -37,20 +37,11 @@ def f(): pass + 2, + 3, + 4, -+ ], ++ ] +) # fmt: on def f(): pass -@@ -14,7 +18,7 @@ - 2, - 3, - 4, -- ] -+ ], - ) - def f(): - pass ``` ## Ruff Output @@ -63,7 +54,7 @@ def f(): pass 2, 3, 4, - ], + ] ) # fmt: on def f(): @@ -76,7 +67,7 @@ def f(): 2, 3, 4, - ], + ] ) def f(): pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap index 5cc5334320..9cd5e7ed31 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff5.py.snap @@ -103,7 +103,7 @@ elif unformatted: - # fmt: on - ] # Includes an formatted indentation. + # fmt: on -+ ], # Includes an formatted indentation. ++ ] # Includes an formatted indentation. }, ) @@ -186,7 +186,7 @@ setup( "foo-bar" "=foo.bar.:main", # fmt: on - ], # Includes an formatted indentation. + ] # Includes an formatted indentation. }, ) diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index eed13bbae7..771b8b6398 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -111,14 +111,14 @@ def __await__(): return (yield) #!/usr/bin/env python3 -import asyncio -import sys -- --from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImport +NOT_YET_IMPLEMENTED_StmtImport --from library import some_connection, some_decorator +-from third_party import X, Y, Z +NOT_YET_IMPLEMENTED_StmtImportFrom +-from library import some_connection, some_decorator +- -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -198,24 +198,6 @@ def __await__(): return (yield) def long_lines(): -@@ -87,7 +94,7 @@ - ast_args.kw_defaults, - parameters, - implicit_default=True, -- ) -+ ), - ) - typedargslist.extend( - gen_annotated_params( -@@ -96,7 +103,7 @@ - parameters, - implicit_default=True, - # trailing standalone comment -- ) -+ ), - ) - _type_comment_re = re.compile( - r""" @@ -135,14 +142,8 @@ a, **kwargs, @@ -334,7 +316,7 @@ def long_lines(): ast_args.kw_defaults, parameters, implicit_default=True, - ), + ) ) typedargslist.extend( gen_annotated_params( @@ -343,7 +325,7 @@ def long_lines(): parameters, implicit_default=True, # trailing standalone comment - ), + ) ) _type_comment_re = re.compile( r""" diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap index 747b512442..fcb6ee100b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function_trailing_comma.py.snap @@ -73,15 +73,6 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -27,7 +27,7 @@ - call( - arg={ - "explode": "this", -- } -+ }, - ) - call2( - arg=[1, 2, 3], @@ -35,7 +35,9 @@ x = { "a": 1, @@ -93,7 +84,7 @@ some_module.some_function( if ( a == { -@@ -47,22 +49,24 @@ +@@ -47,14 +49,16 @@ "f": 6, "g": 7, "h": 8, @@ -114,17 +105,6 @@ some_module.some_function( json = { "k": { "k2": { - "k3": [ - 1, -- ] -- } -- } -+ ], -+ }, -+ }, - } - - @@ -80,18 +84,14 @@ pass @@ -182,7 +162,7 @@ def f( call( arg={ "explode": "this", - }, + } ) call2( arg=[1, 2, 3], @@ -219,9 +199,9 @@ def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "k2": { "k3": [ 1, - ], - }, - }, + ] + } + } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 4e4a09fe83..40a69332f5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -299,9 +299,9 @@ not (aaaaaaaaaaaaaa + {NOT_IMPLEMENTED_set_value for value in NOT_IMPLEMENTED_se [ a + [ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] - in c, + in c ] diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap similarity index 83% rename from crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap rename to crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap index f59509126f..408588813c 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__call.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap @@ -1,6 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/call.py +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py --- ## Input ```py @@ -87,6 +87,11 @@ f( dict() ) +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) ``` ## Output @@ -111,10 +116,10 @@ f(x=2) f(1, x=2) f( - this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd, + this_is_a_very_long_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd ) f( - this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1, + this_is_a_very_long_keyword_argument_asökdhflakjslaslhfdlaffahflahsöfdhasägporejfäalkdsjäfalisjäfdlkasjd=1 ) f( @@ -168,6 +173,12 @@ f( **dict(), # oddly placed own line comment ) + +# Don't add a magic trailing comma when there is only one entry +# Minimized from https://github.com/django/django/blob/7eeadc82c2f7d7a778e3bb43c34d642e6275dacf/django/contrib/admin/checks.py#L674-L681 +f( + a.very_long_function_function_that_is_so_long_that_it_expands_the_parent_but_its_only_a_single_argument() +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap index 6031fbadf8..03d75d057b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__compare.py.snap @@ -180,10 +180,10 @@ return ( ( a + [ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ] >= c - ), + ) ] ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap index 25a854128c..bfafb3e2e7 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__dict.py.snap @@ -69,7 +69,7 @@ a = { # before { # open - key: value, # key # colon # value + key: value # key # colon # value } # close # after @@ -82,7 +82,7 @@ a = { } { - **b, # middle with single item + **b # middle with single item } { diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap index 8930e0036c..e21436a052 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__list.py.snap @@ -14,6 +14,19 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] ``` ## Output @@ -28,6 +41,19 @@ a2 = [ # a a3 = [ # b ] + +# Add magic trailing comma only if there is more than one entry, but respect it if it's +# already there +b1 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +] +b2 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] +b3 = [ + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +] ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap index 0dd8743d48..52be9741ad 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__with.py.snap @@ -113,9 +113,7 @@ with a: # should remove brackets # if we do want to wrap, do we prefer to wrap the entire WithItem or to let the # WithItem allow the `aa + bb` content expression to be wrapped with ( - ( - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ) as c, + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c ): ... From 6acc316d19ad4a9d24a1ed46b6020b84c5a3c28a Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 4 Jul 2023 02:35:16 +0300 Subject: [PATCH 22/27] Turn Linters', etc. implicit `into_iter()`s into explicit `rules()` (#5436) ## Summary As discussed on ~IRC~ Discord, this will make it easier for e.g. the docs generation stuff to get all rules for a linter (using `all_rules()`) instead of just non-nursery ones, and it also makes it more Explicit Is Better Than Implicit to iterate over linter rules. Grepping for `Item = Rule` reveals some remaining implicit `IntoIterator`s that I didn't feel were necessarily in scope for this (and honestly, iterating over a `RuleSet` makes sense). --- crates/ruff/src/flake8_to_ruff/plugin.rs | 2 +- crates/ruff/src/registry.rs | 2 +- crates/ruff/src/rule_selector.rs | 14 ++-- .../src/rules/flake8_type_checking/mod.rs | 2 +- crates/ruff/src/rules/pandas_vet/mod.rs | 6 +- crates/ruff/src/rules/pyflakes/mod.rs | 4 +- crates/ruff_dev/src/generate_rules_table.rs | 4 +- crates/ruff_macros/src/map_codes.rs | 68 +++++++++---------- 8 files changed, 50 insertions(+), 52 deletions(-) diff --git a/crates/ruff/src/flake8_to_ruff/plugin.rs b/crates/ruff/src/flake8_to_ruff/plugin.rs index 77f6645d29..c234556c8a 100644 --- a/crates/ruff/src/flake8_to_ruff/plugin.rs +++ b/crates/ruff/src/flake8_to_ruff/plugin.rs @@ -333,7 +333,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet) -> Vec for selector in selectors { if selector .into_iter() - .any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule)) + .any(|rule| Linter::from(plugin).rules().any(|r| r == rule)) { return true; } diff --git a/crates/ruff/src/registry.rs b/crates/ruff/src/registry.rs index dd47f24815..d53cd6ac7e 100644 --- a/crates/ruff/src/registry.rs +++ b/crates/ruff/src/registry.rs @@ -19,7 +19,7 @@ impl Rule { pub fn from_code(code: &str) -> Result { let (linter, code) = Linter::parse_code(code).ok_or(FromCodeError::Unknown)?; let prefix: RuleCodePrefix = RuleCodePrefix::parse(&linter, code)?; - Ok(prefix.into_iter().next().unwrap()) + Ok(prefix.rules().next().unwrap()) } } diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index 6247346a51..5eb5f1461b 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -158,16 +158,16 @@ impl IntoIterator for &RuleSelector { } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions - .into_iter() - .chain(Linter::McCabe.into_iter()), + .rules() + .chain(Linter::McCabe.rules()), ), RuleSelector::T => RuleSelectorIter::Chain( Linter::Flake8Debugger - .into_iter() - .chain(Linter::Flake8Print.into_iter()), + .rules() + .chain(Linter::Flake8Print.rules()), ), - RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()), - RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()), + RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()), + RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()), } } } @@ -346,7 +346,7 @@ mod clap_completion { let prefix = p.linter().common_prefix(); let code = p.short_code(); - let mut rules_iter = p.into_iter(); + let mut rules_iter = p.rules(); let rule1 = rules_iter.next(); let rule2 = rules_iter.next(); diff --git a/crates/ruff/src/rules/flake8_type_checking/mod.rs b/crates/ruff/src/rules/flake8_type_checking/mod.rs index 06ea6ab839..01aa8c60e4 100644 --- a/crates/ruff/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff/src/rules/flake8_type_checking/mod.rs @@ -327,7 +327,7 @@ mod tests { fn contents(contents: &str, snapshot: &str) { let diagnostics = test_snippet( contents, - &settings::Settings::for_rules(&Linter::Flake8TypeChecking), + &settings::Settings::for_rules(Linter::Flake8TypeChecking.rules()), ); assert_messages!(snapshot, diagnostics); } diff --git a/crates/ruff/src/rules/pandas_vet/mod.rs b/crates/ruff/src/rules/pandas_vet/mod.rs index 2032749fe2..295a83cd24 100644 --- a/crates/ruff/src/rules/pandas_vet/mod.rs +++ b/crates/ruff/src/rules/pandas_vet/mod.rs @@ -353,8 +353,10 @@ mod tests { "PD901_fail_df_var" )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = - test_snippet(contents, &settings::Settings::for_rules(&Linter::PandasVet)); + let diagnostics = test_snippet( + contents, + &settings::Settings::for_rules(Linter::PandasVet.rules()), + ); assert_messages!(snapshot, diagnostics); } diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index 0bdbeaee1d..e796ac4563 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -490,7 +490,7 @@ mod tests { "load_after_unbind_from_class_scope" )] fn contents(contents: &str, snapshot: &str) { - let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes)); + let diagnostics = test_snippet(contents, &Settings::for_rules(Linter::Pyflakes.rules())); assert_messages!(snapshot, diagnostics); } @@ -498,7 +498,7 @@ mod tests { /// Note that all tests marked with `#[ignore]` should be considered TODOs. fn flakes(contents: &str, expected: &[Rule]) { let contents = dedent(contents); - let settings = Settings::for_rules(&Linter::Pyflakes); + let settings = Settings::for_rules(Linter::Pyflakes.rules()); let tokens: Vec = ruff_rustpython::tokenize(&contents); let locator = Locator::new(&contents); let stylist = Stylist::from_tokens(&tokens, &locator); diff --git a/crates/ruff_dev/src/generate_rules_table.rs b/crates/ruff_dev/src/generate_rules_table.rs index 4d29bfea2b..6f1b18cfea 100644 --- a/crates/ruff_dev/src/generate_rules_table.rs +++ b/crates/ruff_dev/src/generate_rules_table.rs @@ -102,10 +102,10 @@ pub(crate) fn generate() -> String { )); table_out.push('\n'); table_out.push('\n'); - generate_table(&mut table_out, prefix, &linter); + generate_table(&mut table_out, prefix.clone().rules(), &linter); } } else { - generate_table(&mut table_out, &linter, &linter); + generate_table(&mut table_out, linter.rules(), &linter); } } diff --git a/crates/ruff_macros/src/map_codes.rs b/crates/ruff_macros/src/map_codes.rs index d37113e0aa..eeee0d7ac9 100644 --- a/crates/ruff_macros/src/map_codes.rs +++ b/crates/ruff_macros/src/map_codes.rs @@ -155,30 +155,13 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { } output.extend(quote! { - impl IntoIterator for &#linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl #linter { + pub fn rules(self) -> ::std::vec::IntoIter { match self { #prefix_into_iter_match_arms } } } }); } - - output.extend(quote! { - impl IntoIterator for &RuleCodePrefix { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - #(RuleCodePrefix::#linter_idents(prefix) => prefix.into_iter(),)* - } - } - } - }); - output.extend(quote! { impl RuleCodePrefix { pub fn parse(linter: &Linter, code: &str) -> Result { @@ -188,6 +171,12 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result { #(Linter::#linter_idents => RuleCodePrefix::#linter_idents(#linter_idents::from_str(code).map_err(|_| crate::registry::FromCodeError::Unknown)?),)* }) } + + pub fn rules(self) -> ::std::vec::IntoIter { + match self { + #(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)* + } + } } }); @@ -344,32 +333,39 @@ fn generate_iter_impl( linter_to_rules: &BTreeMap>, linter_idents: &[&Ident], ) -> TokenStream { - let mut linter_into_iter_match_arms = quote!(); + let mut linter_rules_match_arms = quote!(); + let mut linter_all_rules_match_arms = quote!(); for (linter, map) in linter_to_rules { - let rule_paths = map - .values() - .filter(|rule| { - // Nursery rules have to be explicitly selected, so we ignore them when looking at - // linter-level selectors (e.g., `--select SIM`). - !is_nursery(&rule.group) - }) - .map(|Rule { attrs, path, .. }| { + let rule_paths = map.values().filter(|rule| !is_nursery(&rule.group)).map( + |Rule { attrs, path, .. }| { let rule_name = path.segments.last().unwrap(); quote!(#(#attrs)* Rule::#rule_name) - }); - linter_into_iter_match_arms.extend(quote! { + }, + ); + linter_rules_match_arms.extend(quote! { + Linter::#linter => vec![#(#rule_paths,)*].into_iter(), + }); + let rule_paths = map.values().map(|Rule { attrs, path, .. }| { + let rule_name = path.segments.last().unwrap(); + quote!(#(#attrs)* Rule::#rule_name) + }); + linter_all_rules_match_arms.extend(quote! { Linter::#linter => vec![#(#rule_paths,)*].into_iter(), }); } quote! { - impl IntoIterator for &Linter { - type Item = Rule; - type IntoIter = ::std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { + impl Linter { + /// Rules not in the nursery. + pub fn rules(self: &Linter) -> ::std::vec::IntoIter { match self { - #linter_into_iter_match_arms + #linter_rules_match_arms + } + } + /// All rules, including those in the nursery. + pub fn all_rules(self: &Linter) -> ::std::vec::IntoIter { + match self { + #linter_all_rules_match_arms } } } From 787e2fd49d49b38f4ceb8f5f882caa20050c4f5f Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 4 Jul 2023 09:07:20 +0200 Subject: [PATCH 23/27] Format import statements (#5493) ## Summary Format import statements in all their variants. Specifically, this implemented formatting `StmtImport`, `StmtImportFrom` and `Alias`. ## Test Plan I added some custom snapshots, even though this has been covered well by black's tests. --- .../test/fixtures/ruff/statement/import.py | 3 + .../fixtures/ruff/statement/import_from.py | 16 + .../ruff_python_formatter/src/other/alias.rs | 16 +- .../src/statement/stmt_import.rs | 11 +- .../src/statement/stmt_import_from.rs | 42 ++- ...atibility@miscellaneous__force_pyi.py.snap | 23 +- ...ty@py_310__pattern_matching_extras.py.snap | 27 +- ...tibility@simple_cases__collections.py.snap | 47 +-- ...mpatibility@simple_cases__comments.py.snap | 347 ------------------ ...patibility@simple_cases__comments2.py.snap | 35 +- ...patibility@simple_cases__comments4.py.snap | 22 +- ...patibility@simple_cases__comments5.py.snap | 255 ------------- ...patibility@simple_cases__comments6.py.snap | 8 +- ...cases__comments_non_breaking_space.py.snap | 20 +- ...mpatibility@simple_cases__fmtonoff.py.snap | 29 +- ...patibility@simple_cases__fmtonoff2.py.snap | 9 +- ...lity@simple_cases__fmtpass_imports.py.snap | 114 ------ ...mpatibility@simple_cases__function.py.snap | 22 +- ...patibility@simple_cases__function2.py.snap | 17 +- ...ility@simple_cases__import_spacing.py.snap | 138 +++---- ...@simple_cases__remove_await_parens.py.snap | 8 +- ...move_newline_after_code_block_open.py.snap | 8 +- .../format@expression__attribute.py.snap | 2 +- .../snapshots/format@expression__call.py.snap | 2 +- .../format@statement__import.py.snap | 28 ++ .../format@statement__import_from.py.snap | 46 +++ 26 files changed, 304 insertions(+), 991 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap delete mode 100644 crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py new file mode 100644 index 0000000000..1677400431 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py @@ -0,0 +1,3 @@ +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py new file mode 100644 index 0000000000..335e91036a --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py @@ -0,0 +1,16 @@ +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * diff --git a/crates/ruff_python_formatter/src/other/alias.rs b/crates/ruff_python_formatter/src/other/alias.rs index 8a1501e09c..f59dd012bd 100644 --- a/crates/ruff_python_formatter/src/other/alias.rs +++ b/crates/ruff_python_formatter/src/other/alias.rs @@ -1,5 +1,6 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::Alias; #[derive(Default)] @@ -7,6 +8,15 @@ pub struct FormatAlias; impl FormatNodeRule for FormatAlias { fn fmt_fields(&self, item: &Alias, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let Alias { + range: _, + name, + asname, + } = item; + name.format().fmt(f)?; + if let Some(asname) = asname { + write!(f, [space(), text("as"), space(), asname.format()])?; + } + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import.rs b/crates/ruff_python_formatter/src/statement/stmt_import.rs index 2585dfade7..10aec39721 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import.rs @@ -1,4 +1,5 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; +use crate::{FormatNodeRule, FormattedIterExt, PyFormatter}; +use ruff_formatter::prelude::{format_args, format_with, space, text}; use ruff_formatter::{write, Buffer, FormatResult}; use rustpython_parser::ast::StmtImport; @@ -7,6 +8,12 @@ pub struct FormatStmtImport; impl FormatNodeRule for FormatStmtImport { fn fmt_fields(&self, item: &StmtImport, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImport { names, range: _ } = item; + let names = format_with(|f| { + f.join_with(&format_args![text(","), space()]) + .entries(names.iter().formatted()) + .finish() + }); + write!(f, [text("import"), space(), names]) } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs index bdae4d56ba..ef8cc13584 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_import_from.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_import_from.rs @@ -1,5 +1,7 @@ -use crate::{not_yet_implemented, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::builders::{optional_parentheses, PyFormatterExtensions}; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; +use ruff_formatter::prelude::{dynamic_text, format_with, space, text}; +use ruff_formatter::{write, Buffer, Format, FormatResult}; use rustpython_parser::ast::StmtImportFrom; #[derive(Default)] @@ -7,6 +9,40 @@ pub struct FormatStmtImportFrom; impl FormatNodeRule for FormatStmtImportFrom { fn fmt_fields(&self, item: &StmtImportFrom, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [not_yet_implemented(item)]) + let StmtImportFrom { + module, + names, + range: _, + level, + } = item; + + let level_str = level + .map(|level| ".".repeat(level.to_usize())) + .unwrap_or(String::default()); + + write!( + f, + [ + text("from"), + space(), + dynamic_text(&level_str, None), + module.as_ref().map(AsFormat::format), + space(), + text("import"), + space(), + ] + )?; + if let [name] = names.as_slice() { + // star can't be surrounded by parentheses + if name.name.as_str() == "*" { + return text("*").fmt(f); + } + } + let names = format_with(|f| { + f.join_comma_separated() + .entries(names.iter().map(|name| (name, name.format()))) + .finish() + }); + optional_parentheses(&names).fmt(f) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap index 52fbac942f..f8f00344e5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@miscellaneous__force_pyi.py.snap @@ -43,21 +43,20 @@ def eggs() -> Union[str, int]: ... --- Black +++ Ruff @@ -1,32 +1,58 @@ --from typing import Union -+NOT_YET_IMPLEMENTED_StmtImportFrom -+ + from typing import Union ++ @bird -def zoo(): ... +def zoo(): + ... -+ -+ -+class A: -+ ... -class A: ... ++class A: ++ ... ++ ++ @bar class B: - def BMethod(self) -> None: ... @@ -94,14 +93,14 @@ def eggs() -> Union[str, int]: ... + +class F(A, C): + ... ++ ++ ++def spam() -> None: ++ ... -class F(A, C): ... -def spam() -> None: ... -+def spam() -> None: -+ ... -+ -+ @overload -def spam(arg: str) -> str: ... +def spam(arg: str) -> str: @@ -120,7 +119,7 @@ def eggs() -> Union[str, int]: ... ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from typing import Union @bird diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap index 9afb2c247d..8752abc340 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@py_310__pattern_matching_extras.py.snap @@ -132,8 +132,7 @@ match bar1: --- Black +++ Ruff @@ -1,119 +1,43 @@ --import match -+NOT_YET_IMPLEMENTED_StmtImport + import match -match something: - case [a as b]: @@ -208,10 +207,11 @@ match bar1: - ), - ): - pass -- ++NOT_YET_IMPLEMENTED_StmtMatch + - case [a as match]: - pass -- + - case case: - pass +NOT_YET_IMPLEMENTED_StmtMatch @@ -220,8 +220,9 @@ match bar1: -match match: - case case: - pass -- -- ++NOT_YET_IMPLEMENTED_StmtMatch + + -match a, *b(), c: - case d, *f, g: - pass @@ -236,30 +237,28 @@ match bar1: - pass - case {"maybe": something(complicated as this) as that}: - pass +- +NOT_YET_IMPLEMENTED_StmtMatch - -match something: - case 1 as a: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - case 2 as b, 3 as c: - pass ++NOT_YET_IMPLEMENTED_StmtMatch - case 4 as d, (5 as e), (6 | 7 as g), *h: - pass -+NOT_YET_IMPLEMENTED_StmtMatch - +- -match bar1: - case Foo(aa=Callable() as aa, bb=int()): - print(bar1.aa, bar1.bb) - case _: - print("no match", "\n") -+NOT_YET_IMPLEMENTED_StmtMatch - - +- +- -match bar1: - case Foo( - normal=x, perhaps=[list, {"x": d, "y": 1.0}] as y, otherwise=something, q=t as u @@ -271,7 +270,7 @@ match bar1: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import match NOT_YET_IMPLEMENTED_StmtMatch diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap index 51c59c4e9d..cf66667382 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__collections.py.snap @@ -83,31 +83,9 @@ if True: ```diff --- Black +++ Ruff -@@ -1,40 +1,22 @@ --import core, time, a -+NOT_YET_IMPLEMENTED_StmtImport - --from . import A, B, C -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # keeps existing trailing comma --from foo import ( -- bar, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # also keeps existing structure --from foo import ( -- baz, -- qux, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - # `as` works as well --from foo import ( -- xyzzy as magic, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom +@@ -18,23 +18,12 @@ + xyzzy as magic, + ) -a = { - 1, @@ -132,7 +110,7 @@ if True: nested_no_trailing_comma = {(1, 2, 3), (4, 5, 6)} nested_long_lines = [ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -@@ -52,10 +34,7 @@ +@@ -52,10 +41,7 @@ y = { "oneple": (1,), } @@ -149,18 +127,25 @@ if True: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import core, time, a -NOT_YET_IMPLEMENTED_StmtImportFrom +from . import A, B, C # keeps existing trailing comma -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + bar, +) # also keeps existing structure -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + baz, + qux, +) # `as` works as well -NOT_YET_IMPLEMENTED_StmtImportFrom +from foo import ( + xyzzy as magic, +) a = {1, 2, 3} b = {1, 2, 3} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap deleted file mode 100644 index d78df8d356..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments.py.snap +++ /dev/null @@ -1,347 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments.py ---- -## Input - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -9,16 +9,16 @@ - Possibly also many, many lines. - """ - --import os.path --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport - --import a --from b.c import X # some noqa comment -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - - try: -- import fast -+ NOT_YET_IMPLEMENTED_StmtImport - except ImportError: -- import slow as fast -+ NOT_YET_IMPLEMENTED_StmtImport - - - # Some comment before a function. -@@ -35,7 +35,7 @@ - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. -- import inner_imports -+ NOT_YET_IMPLEMENTED_StmtImport - - if inner_imports.are_evil(): - # Explains why we have this if. -``` - -## Ruff Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImportFrom # some noqa comment - -try: - NOT_YET_IMPLEMENTED_StmtImport -except ImportError: - NOT_YET_IMPLEMENTED_StmtImport - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - NOT_YET_IMPLEMENTED_StmtImport - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - -## Black Output - -```py -#!/usr/bin/env python3 -# fmt: on -# Some license here. -# -# Has many lines. Many, many lines. -# Many, many, many lines. -"""Module docstring. - -Possibly also many, many lines. -""" - -import os.path -import sys - -import a -from b.c import X # some noqa comment - -try: - import fast -except ImportError: - import slow as fast - - -# Some comment before a function. -y = 1 -( - # some strings - y # type: ignore -) - - -def function(default=None): - """Docstring comes first. - - Possibly many lines. - """ - # FIXME: Some comment about why this function is crap but still in production. - import inner_imports - - if inner_imports.are_evil(): - # Explains why we have this if. - # In great detail indeed. - x = X() - return x.method1() # type: ignore - - # This return is also commented for some reason. - return default - - -# Explains why we use global state. -GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} - - -# Another comment! -# This time two lines. - - -class Foo: - """Docstring for class Foo. Example from Sphinx docs.""" - - #: Doc comment for class attribute Foo.bar. - #: It can have multiple lines. - bar = 1 - - flox = 1.5 #: Doc comment for Foo.flox. One line only. - - baz = 2 - """Docstring for class attribute Foo.baz.""" - - def __init__(self): - #: Doc comment for instance attribute qux. - self.qux = 3 - - self.spam = 4 - """Docstring for instance attribute spam.""" - - -#'

This is pweave!

- - -@fast(really=True) -async def wat(): - # This comment, for some reason \ - # contains a trailing backslash. - async with X.open_async() as x: # Some more comments - result = await x.method1() - # Comment after ending a block. - if result: - print("A OK", file=sys.stdout) - # Comment between things. - print() - - -# Some closing comments. -# Maybe Vim or Emacs directives for formatting. -# Who knows. -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap index 23020bfa28..e7ae03c930 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments2.py.snap @@ -177,19 +177,18 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( +@@ -1,8 +1,8 @@ + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( ++ MyLovelyCompanyTeamProjectComponent # NOT DRY + ) + from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom ++ MyLovelyCompanyTeamProjectComponent as component # DRY + ) # Please keep __all__ alphabetized within each category. - -@@ -45,7 +41,7 @@ +@@ -45,7 +45,7 @@ # user-defined types and objects Cheese, Cheese("Wensleydale"), @@ -198,7 +197,7 @@ instruction()#comment with bad spacing ] if "PYTHON" in os.environ: -@@ -60,8 +56,12 @@ +@@ -60,8 +60,12 @@ # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -212,7 +211,7 @@ instruction()#comment with bad spacing children[0], body, children[-1], # type: ignore -@@ -72,7 +72,11 @@ +@@ -72,7 +76,11 @@ body, parameters.children[-1], # )2 ] @@ -225,7 +224,7 @@ instruction()#comment with bad spacing if ( self._proc is not None # has the child process finished? -@@ -114,25 +118,9 @@ +@@ -114,25 +122,9 @@ # yup arg3=True, ) @@ -254,7 +253,7 @@ instruction()#comment with bad spacing while True: if False: continue -@@ -143,7 +131,10 @@ +@@ -143,7 +135,10 @@ # let's return return Node( syms.simple_stmt, @@ -266,7 +265,7 @@ instruction()#comment with bad spacing ) -@@ -158,7 +149,11 @@ +@@ -158,7 +153,11 @@ class Test: def _init_host(self, parsed) -> None: @@ -284,8 +283,12 @@ instruction()#comment with bad spacing ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component # DRY +) # Please keep __all__ alphabetized within each category. diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap index 2358992e6c..7a1948330f 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments4.py.snap @@ -106,19 +106,7 @@ def foo3(list_a, list_b): ```diff --- Black +++ Ruff -@@ -1,9 +1,5 @@ --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent, # NOT DRY --) --from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( -- MyLovelyCompanyTeamProjectComponent as component, # DRY --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - - class C: -@@ -58,37 +54,28 @@ +@@ -58,37 +58,28 @@ def foo(list_a, list_b): results = ( @@ -172,8 +160,12 @@ def foo3(list_a, list_b): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent, # NOT DRY +) +from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( + MyLovelyCompanyTeamProjectComponent as component, # DRY +) class C: diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap deleted file mode 100644 index 28f4426951..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments5.py.snap +++ /dev/null @@ -1,255 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments5.py ---- -## Input - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -27,7 +27,7 @@ - except OSError: - print("problems") - --import sys -+NOT_YET_IMPLEMENTED_StmtImport - - - # leading function comment -``` - -## Ruff Output - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -NOT_YET_IMPLEMENTED_StmtImport - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - -## Black Output - -```py -while True: - if something.changed: - do.stuff() # trailing comment - # Comment belongs to the `if` block. - # This one belongs to the `while` block. - - # Should this one, too? I guess so. - -# This one is properly standalone now. - -for i in range(100): - # first we do this - if i % 33 == 0: - break - - # then we do this - print(i) - # and finally we loop around - -with open(some_temp_file) as f: - data = f.read() - -try: - with open(some_other_file) as w: - w.write(data) - -except OSError: - print("problems") - -import sys - - -# leading function comment -def wat(): - ... - # trailing function comment - - -# SECTION COMMENT - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading 3 -@deco3 -def decorated1(): - ... - - -# leading 1 -@deco1 -# leading 2 -@deco2(with_args=True) -# leading function comment -def decorated1(): - ... - - -# Note: this is fixed in -# Preview.empty_lines_before_class_or_def_with_leading_comments. -# In the current style, the user will have to split those lines by hand. -some_instruction - - -# This comment should be split from `some_instruction` by two lines but isn't. -def g(): - ... - - -if __name__ == "__main__": - main() -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap index 511d9fe1b1..a27f99c5bf 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments6.py.snap @@ -130,12 +130,6 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --from typing import Any, Tuple -+NOT_YET_IMPLEMENTED_StmtImportFrom - - - def f( @@ -49,9 +49,7 @@ element = 0 # type: int another_element = 1 # type: float @@ -192,7 +186,7 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from typing import Any, Tuple def f( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap index 12fd5c1269..e8c73055d0 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__comments_non_breaking_space.py.snap @@ -31,18 +31,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,9 +1,4 @@ --from .config import ( -- ConfigTypeAttributes, -- Int, -- Path, # String, -- # DEFAULT_TYPE_ATTRIBUTES, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom - - result = 1 # A simple comment - result = (1,) # Another one -@@ -14,9 +9,9 @@ +@@ -14,9 +14,9 @@ def function(a: int = 42): @@ -60,7 +49,12 @@ def function(a:int=42): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from .config import ( + ConfigTypeAttributes, + Int, + Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, +) result = 1 # A simple comment result = (1,) # Another one diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap index 32082f2da9..013a97677b 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff.py.snap @@ -198,22 +198,13 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,15 +1,14 @@ - #!/usr/bin/env python3 --import asyncio --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport +@@ -6,10 +6,9 @@ --from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from library import some_connection, some_decorator -+NOT_YET_IMPLEMENTED_StmtImportFrom + from library import some_connection, some_decorator # fmt: off -from third_party import (X, - Y, Z) -+NOT_YET_IMPLEMENTED_StmtImportFrom ++from third_party import X, Y, Z # fmt: on -f"trigger 3.6 mode" +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -336,7 +327,7 @@ d={'a':1, # fmt: off - from hello import a, b - 'unformatted' -+ NOT_YET_IMPLEMENTED_StmtImportFrom ++ from hello import a, b + "unformatted" # fmt: on @@ -433,14 +424,14 @@ d={'a':1, ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom +from library import some_connection, some_decorator # fmt: off -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z # fmt: on NOT_YET_IMPLEMENTED_ExprJoinedStr # Comment 1 @@ -539,7 +530,7 @@ def subscriptlist(): def import_as_names(): # fmt: off - NOT_YET_IMPLEMENTED_StmtImportFrom + from hello import a, b "unformatted" # fmt: on diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap index 56b345fae1..abdfcb6d56 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtonoff2.py.snap @@ -52,12 +52,7 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,40 +1,44 @@ --import pytest -+NOT_YET_IMPLEMENTED_StmtImport - - TmSt = 1 - TmEx = 2 +@@ -5,36 +5,40 @@ # fmt: off @@ -113,7 +108,7 @@ def test_calculate_fades(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import pytest TmSt = 1 TmEx = 2 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap deleted file mode 100644 index 5a0bfd673f..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__fmtpass_imports.py.snap +++ /dev/null @@ -1,114 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtpass_imports.py ---- -## Input - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -import ast -import collections # fmt: skip -import dataclasses -# fmt: off -import os -# fmt: on -import pathlib - -import re # fmt: skip -import secrets - -# fmt: off -import sys -# fmt: on - -import tempfile -import zoneinfo -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -1,19 +1,19 @@ - # Regression test for https://github.com/psf/black/issues/3438 - --import ast --import collections # fmt: skip --import dataclasses -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: off --import os -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: on --import pathlib -+NOT_YET_IMPLEMENTED_StmtImport - --import re # fmt: skip --import secrets -+NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -+NOT_YET_IMPLEMENTED_StmtImport - - # fmt: off --import sys -+NOT_YET_IMPLEMENTED_StmtImport - # fmt: on - --import tempfile --import zoneinfo -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport -``` - -## Ruff Output - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -NOT_YET_IMPLEMENTED_StmtImport -# fmt: off -NOT_YET_IMPLEMENTED_StmtImport -# fmt: on -NOT_YET_IMPLEMENTED_StmtImport - -NOT_YET_IMPLEMENTED_StmtImport # fmt: skip -NOT_YET_IMPLEMENTED_StmtImport - -# fmt: off -NOT_YET_IMPLEMENTED_StmtImport -# fmt: on - -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport -``` - -## Black Output - -```py -# Regression test for https://github.com/psf/black/issues/3438 - -import ast -import collections # fmt: skip -import dataclasses -# fmt: off -import os -# fmt: on -import pathlib - -import re # fmt: skip -import secrets - -# fmt: off -import sys -# fmt: on - -import tempfile -import zoneinfo -``` - - diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap index 771b8b6398..3536f09059 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function.py.snap @@ -107,20 +107,12 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,12 +1,11 @@ - #!/usr/bin/env python3 --import asyncio --import sys -+NOT_YET_IMPLEMENTED_StmtImport -+NOT_YET_IMPLEMENTED_StmtImport +@@ -5,8 +5,7 @@ + from third_party import X, Y, Z --from third_party import X, Y, Z -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from library import some_connection, some_decorator + from library import some_connection, some_decorator - -f"trigger 3.6 mode" -+NOT_YET_IMPLEMENTED_StmtImportFrom +NOT_YET_IMPLEMENTED_ExprJoinedStr @@ -221,12 +213,12 @@ def __await__(): return (yield) ```py #!/usr/bin/env python3 -NOT_YET_IMPLEMENTED_StmtImport -NOT_YET_IMPLEMENTED_StmtImport +import asyncio +import sys -NOT_YET_IMPLEMENTED_StmtImportFrom +from third_party import X, Y, Z -NOT_YET_IMPLEMENTED_StmtImportFrom +from library import some_connection, some_decorator NOT_YET_IMPLEMENTED_ExprJoinedStr diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap index 3fd47e2167..23da8b6351 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__function2.py.snap @@ -65,22 +65,15 @@ with hmm_but_this_should_get_two_preceding_newlines(): ```diff --- Black +++ Ruff -@@ -32,34 +32,28 @@ - - - if os.name == "posix": -- import termios -+ NOT_YET_IMPLEMENTED_StmtImport +@@ -36,7 +36,6 @@ def i_should_be_followed_by_only_one_newline(): pass - elif os.name == "nt": try: -- import msvcrt -+ NOT_YET_IMPLEMENTED_StmtImport - - def i_should_be_followed_by_only_one_newline(): + import msvcrt +@@ -45,21 +44,16 @@ pass except ImportError: @@ -141,13 +134,13 @@ def h(): if os.name == "posix": - NOT_YET_IMPLEMENTED_StmtImport + import termios def i_should_be_followed_by_only_one_newline(): pass elif os.name == "nt": try: - NOT_YET_IMPLEMENTED_StmtImport + import msvcrt def i_should_be_followed_by_only_one_newline(): pass diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap index 6b2e7df8f3..f08a097d01 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__import_spacing.py.snap @@ -61,79 +61,15 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -2,53 +2,31 @@ - - # flake8: noqa - --from logging import WARNING --from logging import ( -- ERROR, --) --import sys -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImport - - # This relies on each of the submodules having an __all__ variable. --from .base_events import * --from .coroutines import * --from .events import * # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here - --from .futures import * --from .locks import * # comment here --from .protocols import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from ..runners import * # comment here --from ..queues import * --from ..streams import * -+NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - --from some_library import ( -- Just, -- Enough, -- Libraries, -- To, -- Fit, -- In, -- This, -- Nice, -- Split, -- Which, -- We, -- No, -- Longer, -- Use, --) --from name_of_a_company.extremely_long_project_name.component.ttypes import ( +@@ -38,7 +38,7 @@ + Use, + ) + from name_of_a_company.extremely_long_project_name.component.ttypes import ( - CuteLittleServiceHandlerFactoryyy, --) --from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom ++ CuteLittleServiceHandlerFactoryyy + ) + from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * --from .a.b.c.subprocess import * --from . import tasks --from . import A, B, C --from . import ( -- SomeVeryLongNameAndAllOfItsAdditionalLetters1, -- SomeVeryLongNameAndAllOfItsAdditionalLetters2, --) -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom -+NOT_YET_IMPLEMENTED_StmtImportFrom - - __all__ = ( - base_events.__all__ ``` ## Ruff Output @@ -143,31 +79,53 @@ __all__ = ( # flake8: noqa -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImport +from logging import WARNING +from logging import ( + ERROR, +) +import sys # This relies on each of the submodules having an __all__ variable. -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here +from .base_events import * +from .coroutines import * +from .events import * # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom +from .futures import * +from .locks import * # comment here +from .protocols import * -NOT_YET_IMPLEMENTED_StmtImportFrom # comment here -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from ..runners import * # comment here +from ..queues import * +from ..streams import * -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from some_library import ( + Just, + Enough, + Libraries, + To, + Fit, + In, + This, + Nice, + Split, + Which, + We, + No, + Longer, + Use, +) +from name_of_a_company.extremely_long_project_name.component.ttypes import ( + CuteLittleServiceHandlerFactoryyy +) +from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom -NOT_YET_IMPLEMENTED_StmtImportFrom +from .a.b.c.subprocess import * +from . import tasks +from . import A, B, C +from . import ( + SomeVeryLongNameAndAllOfItsAdditionalLetters1, + SomeVeryLongNameAndAllOfItsAdditionalLetters2, +) __all__ = ( base_events.__all__ diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap index e77d6934e3..d529f18f49 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_await_parens.py.snap @@ -93,12 +93,6 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --import asyncio -+NOT_YET_IMPLEMENTED_StmtImport - - - # Control example @@ -8,59 +8,70 @@ # Remove brackets for short coroutine/task @@ -219,7 +213,7 @@ async def main(): ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import asyncio # Control example diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap index f9095f8b6f..c97f2a11d6 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__remove_newline_after_code_block_open.py.snap @@ -120,12 +120,6 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,4 +1,4 @@ --import random -+NOT_YET_IMPLEMENTED_StmtImport - - - def foo1(): @@ -27,16 +27,16 @@ @@ -151,7 +145,7 @@ with open("/path/to/file.txt", mode="r") as read_file: ## Ruff Output ```py -NOT_YET_IMPLEMENTED_StmtImport +import random def foo1(): diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap index bbff73657c..2b48c542e5 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__attribute.py.snap @@ -111,7 +111,7 @@ x53 = ( ## Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from argparse import Namespace a = Namespace() diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap index 408588813c..f8a09bc04e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__call.py.snap @@ -96,7 +96,7 @@ f( ## Output ```py -NOT_YET_IMPLEMENTED_StmtImportFrom +from unittest.mock import MagicMock def f(*args, **kwargs): diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap new file mode 100644 index 0000000000..0208606383 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import.py.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +from a import aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd +``` + +## Output +```py +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +``` + + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap new file mode 100644 index 0000000000..0d8c90572e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__import_from.py.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/import_from.py +--- +## Input +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + +## Output +```py +from a import aksjdhflsakhdflkjsadlfajkslhf +from a import ( + aksjdhflsakhdflkjsadlfajkslhf, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa, +) +from a import ( + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as dfgsdfgsd, + aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa as sdkjflsdjlahlfd, +) +from aksjdhflsakhdflkjsadlfajkslhfdkjsaldajlahflashdfljahlfksajlhfajfjfsaahflakjslhdfkjalhdskjfa import * +``` + + + From 937de121f37e3ff32308bef0ba5a6cb438012ea8 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 4 Jul 2023 09:54:35 +0200 Subject: [PATCH 24/27] check-formatter-stability: Remove newlines and add `--error-file` (#5491) ## Summary This makes the output of `check-formatter-stability` more concise by removing extraneous newlines. It also adds a `--error-file` option to that script that allows creating a file with just the errors (without the status messages) to share with others. ## Test Plan I ran it over CPython and looked at the output. I then added the `--error-file` option and looked at the contents of the file --- .../ruff_dev/src/check_formatter_stability.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/crates/ruff_dev/src/check_formatter_stability.rs b/crates/ruff_dev/src/check_formatter_stability.rs index 95a37e6838..b955a7245a 100644 --- a/crates/ruff_dev/src/check_formatter_stability.rs +++ b/crates/ruff_dev/src/check_formatter_stability.rs @@ -4,8 +4,9 @@ //! checking entire repositories. use std::fmt::{Display, Formatter}; -use std::io::stdout; +use std::fs::File; use std::io::Write; +use std::io::{stdout, BufWriter}; use std::panic::catch_unwind; use std::path::{Path, PathBuf}; use std::process::ExitCode; @@ -49,6 +50,9 @@ pub(crate) struct Args { /// Checks each project inside a directory #[arg(long)] pub(crate) multi_project: bool, + /// Write all errors to this file in addition to stdout + #[arg(long)] + pub(crate) error_file: Option, } /// Generate ourself a `try_parse_from` impl for `CheckArgs`. This is a strange way to use clap but @@ -69,6 +73,12 @@ pub(crate) fn main(args: &Args) -> anyhow::Result { #[allow(clippy::print_stdout)] { print!("{}", result.display(args.format)); + println!( + "Found {} stability errors in {} files in {:.2}s", + result.diagnostics.len(), + result.file_count, + result.duration.as_secs_f32(), + ); } result.is_success() @@ -114,6 +124,7 @@ fn check_multi_project(args: &Args) -> bool { match check_repo(&Args { files: vec![path.clone()], + error_file: args.error_file.clone(), ..*args }) { Ok(result) => sender.send(Message::Finished { result, path }), @@ -126,6 +137,9 @@ fn check_multi_project(args: &Args) -> bool { scope.spawn(|_| { let mut stdout = stdout().lock(); + let mut error_file = args.error_file.as_ref().map(|error_file| { + BufWriter::new(File::create(error_file).expect("Couldn't open error file")) + }); for message in receiver { match message { @@ -135,13 +149,19 @@ fn check_multi_project(args: &Args) -> bool { Message::Finished { path, result } => { total_errors += result.diagnostics.len(); total_files += result.file_count; + writeln!( stdout, - "Finished {}\n{}\n", + "Finished {} with {} files in {:.2}s", path.display(), - result.display(args.format) + result.file_count, + result.duration.as_secs_f32(), ) .unwrap(); + write!(stdout, "{}", result.display(args.format)).unwrap(); + if let Some(error_file) = &mut error_file { + write!(error_file, "{}", result.display(args.format)).unwrap(); + } all_success = all_success && result.is_success(); } Message::Failed { path, error } => { @@ -157,8 +177,10 @@ fn check_multi_project(args: &Args) -> bool { #[allow(clippy::print_stdout)] { - println!("{total_errors} stability errors in {total_files} files"); - println!("Finished in {}s", duration.as_secs_f32()); + println!( + "{total_errors} stability errors in {total_files} files in {}s", + duration.as_secs_f32() + ); } all_success @@ -295,23 +317,11 @@ struct DisplayCheckRepoResult<'a> { } impl Display for DisplayCheckRepoResult<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let CheckRepoResult { - duration, - file_count, - diagnostics, - } = self.result; - - for diagnostic in diagnostics { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + for diagnostic in &self.result.diagnostics { write!(f, "{}", diagnostic.display(self.format))?; } - - writeln!( - f, - "Formatting {} files twice took {:.2}s", - file_count, - duration.as_secs_f32() - ) + Ok(()) } } From 0b963ddcfa4e9f7c69103770ea2050117d4314fb Mon Sep 17 00:00:00 2001 From: Thomas de Zeeuw Date: Tue, 4 Jul 2023 16:27:23 +0200 Subject: [PATCH 25/27] Add unreachable code rule (#5384) Co-authored-by: Thomas de Zeeuw Co-authored-by: Micha Reiser --- .gitattributes | 1 + Cargo.lock | 1 + crates/ruff/Cargo.toml | 3 + .../fixtures/control-flow-graph/assert.py | 11 + .../fixtures/control-flow-graph/async-for.py | 41 + .../test/fixtures/control-flow-graph/for.py | 41 + .../test/fixtures/control-flow-graph/if.py | 108 ++ .../test/fixtures/control-flow-graph/match.py | 131 ++ .../test/fixtures/control-flow-graph/raise.py | 5 + .../fixtures/control-flow-graph/simple.py | 23 + .../test/fixtures/control-flow-graph/try.py | 41 + .../test/fixtures/control-flow-graph/while.py | 121 ++ .../resources/test/fixtures/ruff/RUF014.py | 185 +++ crates/ruff/src/checkers/ast/mod.rs | 5 + crates/ruff/src/codes.rs | 2 + crates/ruff/src/rules/ruff/mod.rs | 4 + crates/ruff/src/rules/ruff/rules/mod.rs | 4 + ...les__unreachable__tests__assert.py.md.snap | 97 ++ ...__unreachable__tests__async-for.py.md.snap | 241 ++++ ..._rules__unreachable__tests__for.py.md.snap | 241 ++++ ...__rules__unreachable__tests__if.py.md.snap | 535 ++++++++ ...ules__unreachable__tests__match.py.md.snap | 776 ++++++++++++ ...ules__unreachable__tests__raise.py.md.snap | 41 + ...les__unreachable__tests__simple.py.md.snap | 136 ++ ...ules__unreachable__tests__while.py.md.snap | 527 ++++++++ .../ruff/src/rules/ruff/rules/unreachable.rs | 1101 +++++++++++++++++ ..._rules__ruff__tests__RUF014_RUF014.py.snap | 249 ++++ crates/ruff_dev/src/generate_json_schema.rs | 2 +- crates/ruff_index/src/slice.rs | 12 + crates/ruff_macros/src/newtype_index.rs | 7 +- 30 files changed, 4688 insertions(+), 4 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/assert.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/for.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/if.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/match.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/raise.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/simple.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/try.py create mode 100644 crates/ruff/resources/test/fixtures/control-flow-graph/while.py create mode 100644 crates/ruff/resources/test/fixtures/ruff/RUF014.py create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap create mode 100644 crates/ruff/src/rules/ruff/rules/unreachable.rs create mode 100644 crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap diff --git a/.gitattributes b/.gitattributes index 5bb8d8b736..c4b5fa0751 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ crates/ruff/resources/test/fixtures/isort/line_ending_crlf.py text eol=crlf crates/ruff/resources/test/fixtures/pycodestyle/W605_1.py text eol=crlf ruff.schema.json linguist-generated=true text=auto eol=lf +*.md.snap linguist-language=Markdown diff --git a/Cargo.lock b/Cargo.lock index ca1ef09290..efc811ad0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1865,6 +1865,7 @@ dependencies = [ "result-like", "ruff_cache", "ruff_diagnostics", + "ruff_index", "ruff_macros", "ruff_python_ast", "ruff_python_semantic", diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 350af4bebc..c7a401b79c 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -17,6 +17,7 @@ name = "ruff" [dependencies] ruff_cache = { path = "../ruff_cache" } ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] } +ruff_index = { path = "../ruff_index" } ruff_macros = { path = "../ruff_macros" } ruff_python_whitespace = { path = "../ruff_python_whitespace" } ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] } @@ -88,3 +89,5 @@ colored = { workspace = true, features = ["no-color"] } [features] default = [] schemars = ["dep:schemars"] +# Enables the UnreachableCode rule +unreachable-code = [] diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py new file mode 100644 index 0000000000..bfb3ab9030 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/assert.py @@ -0,0 +1,11 @@ +def func(): + assert True + +def func(): + assert False + +def func(): + assert True, "oops" + +def func(): + assert False, "oops" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py new file mode 100644 index 0000000000..a1dc86a6e9 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/async-for.py @@ -0,0 +1,41 @@ +def func(): + async for i in range(5): + print(i) + +def func(): + async for i in range(20): + print(i) + else: + return 0 + +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 + +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 + +def func(): + async for i in range(12): + continue + +def func(): + async for i in range(1110): + if True: + continue + +def func(): + async for i in range(13): + break + +def func(): + async for i in range(1110): + if True: + break diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/for.py b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py new file mode 100644 index 0000000000..a5807a635a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/for.py @@ -0,0 +1,41 @@ +def func(): + for i in range(5): + print(i) + +def func(): + for i in range(20): + print(i) + else: + return 0 + +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 + +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 + +def func(): + for i in range(12): + continue + +def func(): + for i in range(1110): + if True: + continue + +def func(): + for i in range(13): + break + +def func(): + for i in range(1110): + if True: + break diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/if.py b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py new file mode 100644 index 0000000000..2b5fa42099 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/if.py @@ -0,0 +1,108 @@ +def func(): + if False: + return 0 + return 1 + +def func(): + if True: + return 1 + return 0 + +def func(): + if False: + return 0 + else: + return 1 + +def func(): + if True: + return 1 + else: + return 0 + +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" + +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" + +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" + +def func(): + if False: + return 0 + +def func(): + if True: + return 1 + +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 + +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 + +def func(): + if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5 + return 6 + +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" + +# Test case found in the Bokeh repository that trigger a false positive. +def func(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/match.py b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py new file mode 100644 index 0000000000..cce019e308 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/match.py @@ -0,0 +1,131 @@ +def func(status): + match status: + case _: + return 0 + return "unreachable" + +def func(status): + match status: + case 1: + return 1 + return 0 + +def func(status): + match status: + case 1: + return 1 + case _: + return 0 + +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 + +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 + +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 + +def func(status): + i = 0 + match status, i: + case _, _: + return 0 + +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 + +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") + +def func(point): + match point: + case (0, 0): + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") + +def where_is(point): + class Point: + x: int + y: int + + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") + +def func(points): + match points: + case []: + print("No points") + case [Point(0, 0)]: + print("The origin") + case [Point(x, y)]: + print(f"Single point {x}, {y}") + case [Point(0, y1), Point(0, y2)]: + print(f"Two on the Y axis at {y1}, {y2}") + case _: + print("Something else") + +def func(point): + match point: + case Point(x, y) if x == y: + print(f"Y=X at {x}") + case Point(x, y): + print(f"Not on the diagonal") + +def func(): + from enum import Enum + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) + + match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py new file mode 100644 index 0000000000..37aadc61a0 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/raise.py @@ -0,0 +1,5 @@ +def func(): + raise Exception + +def func(): + raise "a glass!" diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py new file mode 100644 index 0000000000..d1f710149b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/simple.py @@ -0,0 +1,23 @@ +def func(): + pass + +def func(): + pass + +def func(): + return + +def func(): + return 1 + +def func(): + return 1 + return "unreachable" + +def func(): + i = 0 + +def func(): + i = 0 + i += 2 + return i diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/try.py b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py new file mode 100644 index 0000000000..e9f109dfd7 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/try.py @@ -0,0 +1,41 @@ +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + else: + ... + finally: + ... + +def func(): + try: + ... + except Exception: + ... + +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + +def func(): + try: + ... + except Exception: + ... + except OtherException as e: + ... + else: + ... + +def func(): + try: + ... + finally: + ... diff --git a/crates/ruff/resources/test/fixtures/control-flow-graph/while.py b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py new file mode 100644 index 0000000000..6a4174358b --- /dev/null +++ b/crates/ruff/resources/test/fixtures/control-flow-graph/while.py @@ -0,0 +1,121 @@ +def func(): + while False: + return "unreachable" + return 1 + +def func(): + while False: + return "unreachable" + else: + return 1 + +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" + +def func(): + while True: + return 1 + return "unreachable" + +def func(): + while True: + return 1 + else: + return "unreachable" + +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" + +def func(): + i = 0 + while False: + i += 1 + return i + +def func(): + i = 0 + while True: + i += 1 + return i + +def func(): + while True: + pass + return 1 + +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i + +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i + +def func(): + while True: + if True: + return 1 + return 0 + +def func(): + while True: + continue + +def func(): + while False: + continue + +def func(): + while True: + break + +def func(): + while False: + break + +def func(): + while True: + if True: + continue + +def func(): + while True: + if True: + break + +''' +TODO: because `try` statements aren't handled this triggers a false positive as +the last statement is reached, but the rules thinks it isn't (it doesn't +see/process the break statement). + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None: + self.stop_serving = False + while True: + try: + self.server = HTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except OSError: + log.debug(f"port {port} is in use, trying to next one") + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) +''' diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF014.py b/crates/ruff/resources/test/fixtures/ruff/RUF014.py new file mode 100644 index 0000000000..d1ae40f3ca --- /dev/null +++ b/crates/ruff/resources/test/fixtures/ruff/RUF014.py @@ -0,0 +1,185 @@ +def after_return(): + return "reachable" + return "unreachable" + +async def also_works_on_async_functions(): + return "reachable" + return "unreachable" + +def if_always_true(): + if True: + return "reachable" + return "unreachable" + +def if_always_false(): + if False: + return "unreachable" + return "reachable" + +def if_elif_always_false(): + if False: + return "unreachable" + elif False: + return "also unreachable" + return "reachable" + +def if_elif_always_true(): + if False: + return "unreachable" + elif True: + return "reachable" + return "also unreachable" + +def ends_with_if(): + if False: + return "unreachable" + else: + return "reachable" + +def infinite_loop(): + while True: + continue + return "unreachable" + +''' TODO: we could determine these, but we don't yet. +def for_range_return(): + for i in range(10): + if i == 5: + return "reachable" + return "unreachable" + +def for_range_else(): + for i in range(111): + if i == 5: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def for_range_break(): + for i in range(13): + return "reachable" + return "unreachable" + +def for_range_if_break(): + for i in range(1110): + if True: + return "reachable" + return "unreachable" +''' + +def match_wildcard(status): + match status: + case _: + return "reachable" + return "unreachable" + +def match_case_and_wildcard(status): + match status: + case 1: + return "reachable" + case _: + return "reachable" + return "unreachable" + +def raise_exception(): + raise Exception + return "unreachable" + +def while_false(): + while False: + return "unreachable" + return "reachable" + +def while_false_else(): + while False: + return "unreachable" + else: + return "reachable" + +def while_false_else_return(): + while False: + return "unreachable" + else: + return "reachable" + return "also unreachable" + +def while_true(): + while True: + return "reachable" + return "unreachable" + +def while_true_else(): + while True: + return "reachable" + else: + return "unreachable" + +def while_true_else_return(): + while True: + return "reachable" + else: + return "unreachable" + return "also unreachable" + +def while_false_var_i(): + i = 0 + while False: + i += 1 + return i + +def while_true_var_i(): + i = 0 + while True: + i += 1 + return i + +def while_infinite(): + while True: + pass + return "unreachable" + +def while_if_true(): + while True: + if True: + return "reachable" + return "unreachable" + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh1(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data + +''' +TODO: because `try` statements aren't handled this triggers a false positive as +the last statement is reached, but the rules thinks it isn't (it doesn't +see/process the break statement). + +# Test case found in the Bokeh repository that trigger a false positive. +def bokeh2(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> None: + self.stop_serving = False + while True: + try: + self.server = HTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except OSError: + log.debug(f"port {port} is in use, trying to next one") + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) +''' diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 6eb1f6b25c..c4f3617120 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -619,6 +619,11 @@ where ); } } + #[cfg(feature = "unreachable-code")] + if self.enabled(Rule::UnreachableCode) { + self.diagnostics + .extend(ruff::rules::unreachable::in_function(name, body)); + } } Stmt::Return(_) => { if self.enabled(Rule::ReturnOutsideFunction) { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 79a7982dc9..5e526f599d 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -761,6 +761,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension), (Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault), (Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional), + #[cfg(feature = "unreachable-code")] + (Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode), (Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA), (Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml), diff --git a/crates/ruff/src/rules/ruff/mod.rs b/crates/ruff/src/rules/ruff/mod.rs index f0aa41c570..a110328a24 100644 --- a/crates/ruff/src/rules/ruff/mod.rs +++ b/crates/ruff/src/rules/ruff/mod.rs @@ -30,6 +30,10 @@ mod tests { #[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))] #[test_case(Rule::PairwiseOverZipped, Path::new("RUF007.py"))] #[test_case(Rule::StaticKeyDictComprehension, Path::new("RUF011.py"))] + #[cfg_attr( + feature = "unreachable-code", + test_case(Rule::UnreachableCode, Path::new("RUF014.py")) + )] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff/src/rules/ruff/rules/mod.rs b/crates/ruff/src/rules/ruff/rules/mod.rs index 6ec0eda590..f79f5fafd7 100644 --- a/crates/ruff/src/rules/ruff/rules/mod.rs +++ b/crates/ruff/src/rules/ruff/rules/mod.rs @@ -9,6 +9,8 @@ pub(crate) use mutable_class_default::*; pub(crate) use mutable_dataclass_default::*; pub(crate) use pairwise_over_zipped::*; pub(crate) use static_key_dict_comprehension::*; +#[cfg(feature = "unreachable-code")] +pub(crate) use unreachable::*; pub(crate) use unused_noqa::*; mod ambiguous_unicode_character; @@ -24,6 +26,8 @@ mod mutable_class_default; mod mutable_dataclass_default; mod pairwise_over_zipped; mod static_key_dict_comprehension; +#[cfg(feature = "unreachable-code")] +pub(crate) mod unreachable; mod unused_noqa; #[derive(Clone, Copy)] diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap new file mode 100644 index 0000000000..17ca4671a0 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__assert.py.md.snap @@ -0,0 +1,97 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + assert True +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + assert False +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + assert True, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert True, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + assert False, "oops" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1[["Exception raised"]] + block2["assert False, #quot;oops#quot;\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap new file mode 100644 index 0000000000..431c82d33c --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__async-for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + async for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["async for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + async for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["async for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + async for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["async for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + async for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["async for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + async for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["async for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + async for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["async for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + async for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["async for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + async for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["async for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap new file mode 100644 index 0000000000..f3d3def743 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__for.py.md.snap @@ -0,0 +1,241 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + for i in range(5): + print(i) +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(i)\n"] + block2["for i in range(5): + print(i)\n"] + + start --> block2 + block2 -- "range(5)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + for i in range(20): + print(i) + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["print(i)\n"] + block1["return 0\n"] + block2["for i in range(20): + print(i) + else: + return 0\n"] + + start --> block2 + block2 -- "range(20)" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + for i in range(10): + if i == 5: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["for i in range(10): + if i == 5: + return 1\n"] + + start --> block3 + block3 -- "range(10)" --> block2 + block3 -- "else" --> block0 + block2 -- "i == 5" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + for i in range(111): + if i == 5: + return 1 + else: + return 0 + return 2 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 2\n"] + block1["return 1\n"] + block2["if i == 5: + return 1\n"] + block3["return 0\n"] + block4["for i in range(111): + if i == 5: + return 1 + else: + return 0\n"] + + start --> block4 + block4 -- "range(111)" --> block2 + block4 -- "else" --> block3 + block3 --> return + block2 -- "i == 5" --> block1 + block2 -- "else" --> block4 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + for i in range(12): + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["for i in range(12): + continue\n"] + + start --> block2 + block2 -- "range(12)" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + for i in range(1110): + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["for i in range(1110): + if True: + continue\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + for i in range(13): + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["for i in range(13): + break\n"] + + start --> block2 + block2 -- "range(13)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + for i in range(1110): + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["for i in range(1110): + if True: + break\n"] + + start --> block3 + block3 -- "range(1110)" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap new file mode 100644 index 0000000000..1ab3a11c7d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__if.py.md.snap @@ -0,0 +1,535 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + if False: + return 0 + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if False: + return 0 + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 0\n"] + block2["if True: + return 1 + else: + return 0\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + if False: + return 0 + else: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["return 1\n"] + block3["if False: + return 0 + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + if True: + return 1 + else: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["return 0\n"] + block3["if True: + return 1 + else: + return 0\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + if True: + if True: + return 1 + return 2 + else: + return 3 + return "unreachable2" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable2#quot;\n"] + block1["return 2\n"] + block2["return 1\n"] + block3["if True: + return 1\n"] + block4["return 3\n"] + block5["if True: + if True: + return 1 + return 2 + else: + return 3\n"] + + start --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + if False: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["if False: + return 0\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + if True: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 1\n"] + block2["if True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + if True: + return 1 + elif False: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif False: + return 2 + else: + return 0\n"] + block4["if True: + return 1 + elif False: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "True" --> block0 + block4 -- "else" --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + if False: + return 1 + elif True: + return 2 + else: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return 2\n"] + block2["return 0\n"] + block3["elif True: + return 2 + else: + return 0\n"] + block4["if False: + return 1 + elif True: + return 2 + else: + return 0\n"] + + start --> block4 + block4 -- "False" --> block0 + block4 -- "else" --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 3\n"] + block2["return 0\n"] + block3["return 1\n"] + block4["return 2\n"] + block5["elif True: + return 1 + else: + return 2\n"] + block6["if False: + return 0 + elif True: + return 1 + else: + return 2\n"] + block7["return 4\n"] + block8["return 5\n"] + block9["elif True: + return 4 + else: + return 5\n"] + block10["if True: + if False: + return 0 + elif True: + return 1 + else: + return 2 + return 3 + elif True: + return 4 + else: + return 5\n"] + + start --> block10 + block10 -- "True" --> block6 + block10 -- "else" --> block9 + block9 -- "True" --> block7 + block9 -- "else" --> block8 + block8 --> return + block7 --> return + block6 -- "False" --> block2 + block6 -- "else" --> block5 + block5 -- "True" --> block3 + block5 -- "else" --> block4 + block4 --> return + block3 --> return + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + if False: + return "unreached" + elif False: + return "also unreached" + return "reached" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;reached#quot;\n"] + block1["return #quot;unreached#quot;\n"] + block2["return #quot;also unreached#quot;\n"] + block3["elif False: + return #quot;also unreached#quot;\n"] + block4["if False: + return #quot;unreached#quot; + elif False: + return #quot;also unreached#quot;\n"] + + start --> block4 + block4 -- "False" --> block1 + block4 -- "else" --> block3 + block3 -- "False" --> block2 + block3 -- "else" --> block0 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 13 +### Source +```python +def func(self, obj: BytesRep) -> bytes: + data = obj["data"] + + if isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data["id"] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f"can't resolve buffer '{id}'") + + return buffer.data +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return buffer.data\n"] + block1["return base64.b64decode(data)\n"] + block2["buffer = data\n"] + block3["buffer = self._buffers[id]\n"] + block4["self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block5["id = data[#quot;id#quot;]\nif id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block6["elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + block7["data = obj[#quot;data#quot;]\nif isinstance(data, str): + return base64.b64decode(data) + elif isinstance(data, Buffer): + buffer = data + else: + id = data[#quot;id#quot;] + + if id in self._buffers: + buffer = self._buffers[id] + else: + self.error(f#quot;can't resolve buffer '{id}'#quot;)\n"] + + start --> block7 + block7 -- "isinstance(data, str)" --> block1 + block7 -- "else" --> block6 + block6 -- "isinstance(data, Buffer)" --> block2 + block6 -- "else" --> block5 + block5 -- "id in self._buffers" --> block3 + block5 -- "else" --> block4 + block4 --> block0 + block3 --> block0 + block2 --> block0 + block1 --> return + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap new file mode 100644 index 0000000000..d8a6ddb59b --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__match.py.md.snap @@ -0,0 +1,776 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(status): + match status: + case _: + return 0 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 0\n"] + block2["match status: + case _: + return 0\n"] + + start --> block2 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(status): + match status: + case 1: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["match status: + case 1: + return 1\n"] + + start --> block2 + block2 -- "case 1" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(status): + match status: + case 1: + return 1 + case _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["match status: + case 1: + return 1 + case _: + return 0\n"] + block2["return 1\n"] + block3["match status: + case 1: + return 1 + case _: + return 0\n"] + + start --> block3 + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + return 6 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 6\n"] + block1["return 5\n"] + block2["match status: + case 1 | 2 | 3: + return 5\n"] + + start --> block2 + block2 -- "case 1 | 2 | 3" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(status): + match status: + case 1 | 2 | 3: + return 5 + case _: + return 10 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 10\n"] + block2["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + block3["return 5\n"] + block4["match status: + case 1 | 2 | 3: + return 5 + case _: + return 10\n"] + + start --> block4 + block4 -- "case 1 | 2 | 3" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(status): + match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return "1 again" + case _: + return 3 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 3\n"] + block1["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block2["return #quot;1 again#quot;\n"] + block3["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block4["return 1\n"] + block5["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + block6["return 0\n"] + block7["match status: + case 0: + return 0 + case 1: + return 1 + case 1: + return #quot;1 again#quot; + case _: + return 3\n"] + + start --> block7 + block7 -- "case 0" --> block6 + block7 -- "else" --> block5 + block6 --> return + block5 -- "case 1" --> block4 + block5 -- "else" --> block3 + block4 --> return + block3 -- "case 1" --> block2 + block3 -- "else" --> block1 + block2 --> return + block1 --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, _: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, _: + return 0\n"] + block3["i = 0\n"] + + start --> block3 + block3 --> block2 + block2 -- "case _, _" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 7 +### Source +```python +def func(status): + i = 0 + match status, i: + case _, 0: + return 0 + case _, 2: + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["return 0\n"] + block2["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block3["return 0\n"] + block4["match status, i: + case _, 0: + return 0 + case _, 2: + return 0\n"] + block5["i = 0\n"] + + start --> block5 + block5 --> block4 + block4 -- "case _, 0" --> block3 + block4 -- "else" --> block2 + block3 --> return + block2 -- "case _, 2" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 8 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case _: + raise ValueError("oops") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;oops#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + block3["print(#quot;Origin#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case _: + raise ValueError(#quot;oops#quot;)\n"] + + start --> block4 + block4 -- "case (0, 0)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 9 +### Source +```python +def func(point): + match point: + case (0, 0): + print("Origin") + case (0, y): + print(f"Y={y}") + case (x, 0): + print(f"X={x}") + case (x, y): + print(f"X={x}, Y={y}") + case _: + raise ValueError("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["raise ValueError(#quot;Not a point#quot;)\n"] + block2["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block3["print(f#quot;X={x}, Y={y}#quot;)\n"] + block4["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case (0, 0): + print(#quot;Origin#quot;) + case (0, y): + print(f#quot;Y={y}#quot;) + case (x, 0): + print(f#quot;X={x}#quot;) + case (x, y): + print(f#quot;X={x}, Y={y}#quot;) + case _: + raise ValueError(#quot;Not a point#quot;)\n"] + + start --> block10 + block10 -- "case (0, 0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case (0, y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case (x, 0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case (x, y)" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> return + block0 --> return +``` + +## Function 10 +### Source +```python +def where_is(point): + class Point: + x: int + y: int + + match point: + case Point(x=0, y=0): + print("Origin") + case Point(x=0, y=y): + print(f"Y={y}") + case Point(x=x, y=0): + print(f"X={x}") + case Point(): + print("Somewhere else") + case _: + print("Not a point") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Not a point#quot;)\n"] + block2["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block3["print(#quot;Somewhere else#quot;)\n"] + block4["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block5["print(f#quot;X={x}#quot;)\n"] + block6["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block7["print(f#quot;Y={y}#quot;)\n"] + block8["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block9["print(#quot;Origin#quot;)\n"] + block10["match point: + case Point(x=0, y=0): + print(#quot;Origin#quot;) + case Point(x=0, y=y): + print(f#quot;Y={y}#quot;) + case Point(x=x, y=0): + print(f#quot;X={x}#quot;) + case Point(): + print(#quot;Somewhere else#quot;) + case _: + print(#quot;Not a point#quot;)\n"] + block11["class Point: + x: int + y: int\n"] + + start --> block11 + block11 --> block10 + block10 -- "case Point(x=0, y=0)" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case Point(x=0, y=y)" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case Point(x=x, y=0)" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Point()" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(points): + match points: + case []: + print("No points") + case [Point(0, 0)]: + print("The origin") + case [Point(x, y)]: + print(f"Single point {x}, {y}") + case [Point(0, y1), Point(0, y2)]: + print(f"Two on the Y axis at {y1}, {y2}") + case _: + print("Something else") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;Something else#quot;)\n"] + block2["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block3["print(f#quot;Two on the Y axis at {y1}, {y2}#quot;)\n"] + block4["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block5["print(f#quot;Single point {x}, {y}#quot;)\n"] + block6["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block7["print(#quot;The origin#quot;)\n"] + block8["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + block9["print(#quot;No points#quot;)\n"] + block10["match points: + case []: + print(#quot;No points#quot;) + case [Point(0, 0)]: + print(#quot;The origin#quot;) + case [Point(x, y)]: + print(f#quot;Single point {x}, {y}#quot;) + case [Point(0, y1), Point(0, y2)]: + print(f#quot;Two on the Y axis at {y1}, {y2}#quot;) + case _: + print(#quot;Something else#quot;)\n"] + + start --> block10 + block10 -- "case []" --> block9 + block10 -- "else" --> block8 + block9 --> block0 + block8 -- "case [Point(0, 0)]" --> block7 + block8 -- "else" --> block6 + block7 --> block0 + block6 -- "case [Point(x, y)]" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case [Point(0, y1), Point(0, y2)]" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 --> block1 + block1 --> block0 + block0 --> return +``` + +## Function 12 +### Source +```python +def func(point): + match point: + case Point(x, y) if x == y: + print(f"Y=X at {x}") + case Point(x, y): + print(f"Not on the diagonal") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(f#quot;Not on the diagonal#quot;)\n"] + block2["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + block3["print(f#quot;Y=X at {x}#quot;)\n"] + block4["match point: + case Point(x, y) if x == y: + print(f#quot;Y=X at {x}#quot;) + case Point(x, y): + print(f#quot;Not on the diagonal#quot;)\n"] + + start --> block4 + block4 -- "case Point(x, y) if x == y" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Point(x, y)" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + from enum import Enum + class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue' + + color = Color(input("Enter your choice of 'red', 'blue' or 'green': ")) + + match color: + case Color.RED: + print("I see red!") + case Color.GREEN: + print("Grass is green") + case Color.BLUE: + print("I'm feeling the blues :(") +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["print(#quot;I'm feeling the blues :(#quot;)\n"] + block2["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block3["print(#quot;Grass is green#quot;)\n"] + block4["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block5["print(#quot;I see red!#quot;)\n"] + block6["match color: + case Color.RED: + print(#quot;I see red!#quot;) + case Color.GREEN: + print(#quot;Grass is green#quot;) + case Color.BLUE: + print(#quot;I'm feeling the blues :(#quot;)\n"] + block7["from enum import Enum\nclass Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 'blue'\ncolor = Color(input(#quot;Enter your choice of 'red', 'blue' or 'green': #quot;))\n"] + + start --> block7 + block7 --> block6 + block6 -- "case Color.RED" --> block5 + block6 -- "else" --> block4 + block5 --> block0 + block4 -- "case Color.GREEN" --> block3 + block4 -- "else" --> block2 + block3 --> block0 + block2 -- "case Color.BLUE" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap new file mode 100644 index 0000000000..7da998458d --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__raise.py.md.snap @@ -0,0 +1,41 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + raise Exception +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise Exception\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + raise "a glass!" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["raise #quot;a glass!#quot;\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap new file mode 100644 index 0000000000..881df6fad1 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__simple.py.md.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + pass +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["pass\n"] + + start --> block0 + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + return +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return\n"] + + start --> block0 + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + + start --> block0 + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + + start --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + i = 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\n"] + + start --> block0 + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + i += 2 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["i = 0\ni += 2\nreturn i\n"] + + start --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap new file mode 100644 index 0000000000..aa030a03a4 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/snapshots/ruff__rules__ruff__rules__unreachable__tests__while.py.md.snap @@ -0,0 +1,527 @@ +--- +source: crates/ruff/src/rules/ruff/rules/unreachable.rs +description: "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." +--- +## Function 0 +### Source +```python +def func(): + while False: + return "unreachable" + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while False: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 1 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block2 + block2 -- "False" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 2 +### Source +```python +def func(): + while False: + return "unreachable" + else: + return 1 + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return #quot;unreachable#quot;\n"] + block2["return 1\n"] + block3["while False: + return #quot;unreachable#quot; + else: + return 1\n"] + + start --> block3 + block3 -- "False" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 3 +### Source +```python +def func(): + while True: + return 1 + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;unreachable#quot;\n"] + block1["return 1\n"] + block2["while True: + return 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> return + block0 --> return +``` + +## Function 4 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["return #quot;unreachable#quot;\n"] + block2["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block2 + block2 -- "True" --> block0 + block2 -- "else" --> block1 + block1 --> return + block0 --> return +``` + +## Function 5 +### Source +```python +def func(): + while True: + return 1 + else: + return "unreachable" + return "also unreachable" +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return #quot;also unreachable#quot;\n"] + block1["return 1\n"] + block2["return #quot;unreachable#quot;\n"] + block3["while True: + return 1 + else: + return #quot;unreachable#quot;\n"] + + start --> block3 + block3 -- "True" --> block1 + block3 -- "else" --> block2 + block2 --> return + block1 --> return + block0 --> return +``` + +## Function 6 +### Source +```python +def func(): + i = 0 + while False: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile False: + i += 1\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 7 +### Source +```python +def func(): + i = 0 + while True: + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["i = 0\nwhile True: + i += 1\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 8 +### Source +```python +def func(): + while True: + pass + return 1 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 1\n"] + block1["pass\n"] + block2["while True: + pass\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 9 +### Source +```python +def func(): + i = 0 + while True: + if True: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if True: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if True: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "True" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 10 +### Source +```python +def func(): + i = 0 + while True: + if False: + print("ok") + i += 1 + return i +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return i\n"] + block1["i += 1\n"] + block2["print(#quot;ok#quot;)\n"] + block3["if False: + print(#quot;ok#quot;)\n"] + block4["i = 0\nwhile True: + if False: + print(#quot;ok#quot;) + i += 1\n"] + + start --> block4 + block4 -- "True" --> block3 + block4 -- "else" --> block0 + block3 -- "False" --> block2 + block3 -- "else" --> block1 + block2 --> block1 + block1 --> block4 + block0 --> return +``` + +## Function 11 +### Source +```python +def func(): + while True: + if True: + return 1 + return 0 +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0["return 0\n"] + block1["return 1\n"] + block2["if True: + return 1\n"] + block3["while True: + if True: + return 1\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> return + block0 --> return +``` + +## Function 12 +### Source +```python +def func(): + while True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while True: + continue\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 13 +### Source +```python +def func(): + while False: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["while False: + continue\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block2 + block0 --> return +``` + +## Function 14 +### Source +```python +def func(): + while True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while True: + break\n"] + + start --> block2 + block2 -- "True" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 15 +### Source +```python +def func(): + while False: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["while False: + break\n"] + + start --> block2 + block2 -- "False" --> block1 + block2 -- "else" --> block0 + block1 --> block0 + block0 --> return +``` + +## Function 16 +### Source +```python +def func(): + while True: + if True: + continue +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["continue\n"] + block2["if True: + continue\n"] + block3["while True: + if True: + continue\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block3 + block0 --> return +``` + +## Function 17 +### Source +```python +def func(): + while True: + if True: + break +``` + +### Control Flow Graph +```mermaid +flowchart TD + start(("Start")) + return(("End")) + block0[["`*(empty)*`"]] + block1["break\n"] + block2["if True: + break\n"] + block3["while True: + if True: + break\n"] + + start --> block3 + block3 -- "True" --> block2 + block3 -- "else" --> block0 + block2 -- "True" --> block1 + block2 -- "else" --> block3 + block1 --> block0 + block0 --> return +``` + + diff --git a/crates/ruff/src/rules/ruff/rules/unreachable.rs b/crates/ruff/src/rules/ruff/rules/unreachable.rs new file mode 100644 index 0000000000..8aede6a613 --- /dev/null +++ b/crates/ruff/src/rules/ruff/rules/unreachable.rs @@ -0,0 +1,1101 @@ +use std::{fmt, iter, usize}; + +use log::error; +use rustpython_parser::ast::{ + Expr, Identifier, MatchCase, Pattern, PatternMatchAs, Ranged, Stmt, StmtAsyncFor, + StmtAsyncWith, StmtFor, StmtMatch, StmtReturn, StmtTry, StmtTryStar, StmtWhile, StmtWith, +}; +use rustpython_parser::text_size::{TextRange, TextSize}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_index::{IndexSlice, IndexVec}; +use ruff_macros::{derive_message_formats, newtype_index, violation}; + +/// ## What it does +/// Checks for unreachable code. +/// +/// ## Why is this bad? +/// Unreachable code can be a maintenance burden without ever being used. +/// +/// ## Example +/// ```python +/// def function(): +/// if False: +/// return "unreachable" +/// return "reachable" +/// ``` +/// +/// Use instead: +/// ```python +/// def function(): +/// return "reachable" +/// ``` +#[violation] +pub struct UnreachableCode { + name: String, +} + +impl Violation for UnreachableCode { + #[derive_message_formats] + fn message(&self) -> String { + let UnreachableCode { name } = self; + format!("Unreachable code in {name}") + } +} + +pub(crate) fn in_function(name: &Identifier, body: &[Stmt]) -> Vec { + // Create basic code blocks from the body. + let basic_blocks = BasicBlocks::from(body); + + // Basic on the code blocks we can (more) easily follow what statements are + // and aren't reached, we'll mark them as such in `reached_map`. + let mut reached_map = Bitmap::with_capacity(basic_blocks.len()); + + if let Some(start_index) = basic_blocks.start_index() { + mark_reached(&mut reached_map, &basic_blocks.blocks, start_index); + } + + // For each unreached code block create a diagnostic. + reached_map + .unset() + .filter_map(|idx| { + let block = &basic_blocks.blocks[idx]; + if block.is_sentinel() { + return None; + } + + // TODO: add more information to the diagnostic. Include the entire + // code block, not just the first line. Maybe something to indicate + // the code flow and where it prevents this block from being reached + // for example. + let Some(stmt) = block.stmts.first() else { + // This should never happen. + error!("Got an unexpected empty code block"); + return None; + }; + Some(Diagnostic::new( + UnreachableCode { + name: name.as_str().to_owned(), + }, + stmt.range(), + )) + }) + .collect() +} + +/// Simple bitmap. +#[derive(Debug)] +struct Bitmap { + bits: Box<[usize]>, + capacity: usize, +} + +impl Bitmap { + /// Create a new `Bitmap` with `capacity` capacity. + fn with_capacity(capacity: usize) -> Bitmap { + let mut size = capacity / usize::BITS as usize; + if (capacity % usize::BITS as usize) != 0 { + size += 1; + } + Bitmap { + bits: vec![0; size].into_boxed_slice(), + capacity, + } + } + + /// Set bit at index `idx` to true. + /// + /// Returns a boolean indicating if the bit was already set. + fn set(&mut self, idx: BlockIndex) -> bool { + let bits_index = (idx.as_u32() / usize::BITS) as usize; + let shift = idx.as_u32() % usize::BITS; + if (self.bits[bits_index] & (1 << shift)) == 0 { + self.bits[bits_index] |= 1 << shift; + false + } else { + true + } + } + + /// Returns an iterator of all unset indices. + fn unset(&self) -> impl Iterator + '_ { + let mut index = 0; + let mut shift = 0; + let last_max_shift = self.capacity % usize::BITS as usize; + iter::from_fn(move || loop { + if shift >= usize::BITS as usize { + shift = 0; + index += 1; + } + if self.bits.len() <= index || (index >= self.bits.len() - 1 && shift >= last_max_shift) + { + return None; + } + + let is_set = (self.bits[index] & (1 << shift)) != 0; + shift += 1; + if !is_set { + return Some(BlockIndex::from_usize( + (index * usize::BITS as usize) + shift - 1, + )); + } + }) + } +} + +/// Set bits in `reached_map` for all blocks that are reached in `blocks` +/// starting with block at index `idx`. +fn mark_reached( + reached_map: &mut Bitmap, + blocks: &IndexSlice>, + start_index: BlockIndex, +) { + let mut idx = start_index; + + loop { + let block = &blocks[idx]; + if reached_map.set(idx) { + return; // Block already visited, no needed to do it again. + } + + match &block.next { + NextBlock::Always(next) => idx = *next, + NextBlock::If { + condition, + next, + orelse, + } => { + match taken(condition) { + Some(true) => idx = *next, // Always taken. + Some(false) => idx = *orelse, // Never taken. + None => { + // Don't know, both branches might be taken. + idx = *next; + mark_reached(reached_map, blocks, *orelse); + } + } + } + NextBlock::Terminate => return, + } + } +} + +/// Determines if `condition` is taken. +/// Returns `Some(true)` if the condition is always true, e.g. `if True`, same +/// with `Some(false)` if it's never taken. If it can't be determined it returns +/// `None`, e.g. `If i == 100`. +fn taken(condition: &Condition) -> Option { + // TODO: add more cases to this where we can determine a condition + // statically. For now we only consider constant booleans. + match condition { + Condition::Test(expr) => match expr { + Expr::Constant(constant) => constant.value.as_bool().copied(), + _ => None, + }, + Condition::Iterator(_) => None, + Condition::Match { .. } => None, + } +} + +/// Index into [`BasicBlocks::blocks`]. +#[newtype_index] +#[derive(PartialOrd, Ord)] +struct BlockIndex; + +/// Collection of basic block. +#[derive(Debug, PartialEq)] +struct BasicBlocks<'stmt> { + /// # Notes + /// + /// The order of these block is unspecified. However it's guaranteed that + /// the last block is the first statement in the function and the first + /// block is the last statement. The block are more or less in reverse + /// order, but it gets fussy around control flow statements (e.g. `while` + /// statements). + /// + /// For loop blocks, and similar recurring control flows, the end of the + /// body will point to the loop block again (to create the loop). However an + /// oddity here is that this block might contain statements before the loop + /// itself which, of course, won't be executed again. + /// + /// For example: + /// ```python + /// i = 0 # block 0 + /// while True: # + /// continue # block 1 + /// ``` + /// Will create a connection between block 1 (loop body) and block 0, which + /// includes the `i = 0` statement. + /// + /// To keep `NextBlock` simple(r) `NextBlock::If`'s `next` and `orelse` + /// fields only use `BlockIndex`, which means that they can't terminate + /// themselves. To support this we insert *empty*/fake blocks before the end + /// of the function that we can link to. + /// + /// Finally `BasicBlock` can also be a sentinel node, see the associated + /// constants of [`BasicBlock`]. + blocks: IndexVec>, +} + +impl BasicBlocks<'_> { + fn len(&self) -> usize { + self.blocks.len() + } + + fn start_index(&self) -> Option { + self.blocks.indices().last() + } +} + +impl<'stmt> From<&'stmt [Stmt]> for BasicBlocks<'stmt> { + /// # Notes + /// + /// This assumes that `stmts` is a function body. + fn from(stmts: &'stmt [Stmt]) -> BasicBlocks<'stmt> { + let mut blocks = BasicBlocksBuilder::with_capacity(stmts.len()); + + blocks.create_blocks(stmts, None); + + blocks.finish() + } +} + +/// Basic code block, sequence of statements unconditionally executed +/// "together". +#[derive(Debug, PartialEq)] +struct BasicBlock<'stmt> { + stmts: &'stmt [Stmt], + next: NextBlock<'stmt>, +} + +/// Edge between basic blocks (in the control-flow graph). +#[derive(Debug, PartialEq)] +enum NextBlock<'stmt> { + /// Always continue with a block. + Always(BlockIndex), + /// Condition jump. + If { + /// Condition that needs to be evaluated to jump to the `next` or + /// `orelse` block. + condition: Condition<'stmt>, + /// Next block if `condition` is true. + next: BlockIndex, + /// Next block if `condition` is false. + orelse: BlockIndex, + }, + /// The end. + Terminate, +} + +/// Condition used to determine to take the `next` or `orelse` branch in +/// [`NextBlock::If`]. +#[derive(Clone, Debug, PartialEq)] +enum Condition<'stmt> { + /// Conditional statement, this should evaluate to a boolean, for e.g. `if` + /// or `while`. + Test(&'stmt Expr), + /// Iterator for `for` statements, e.g. for `i in range(10)` this will be + /// `range(10)`. + Iterator(&'stmt Expr), + Match { + /// `match $subject`. + subject: &'stmt Expr, + /// `case $case`, include pattern, guard, etc. + case: &'stmt MatchCase, + }, +} + +impl<'stmt> Ranged for Condition<'stmt> { + fn range(&self) -> TextRange { + match self { + Condition::Test(expr) | Condition::Iterator(expr) => expr.range(), + // The case of the match statement, without the body. + Condition::Match { subject: _, case } => TextRange::new( + case.start(), + case.guard + .as_ref() + .map_or(case.pattern.end(), |guard| guard.end()), + ), + } + } +} + +impl<'stmt> BasicBlock<'stmt> { + /// A sentinel block indicating an empty termination block. + const EMPTY: BasicBlock<'static> = BasicBlock { + stmts: &[], + next: NextBlock::Terminate, + }; + + /// A sentinel block indicating an exception was raised. + const EXCEPTION: BasicBlock<'static> = BasicBlock { + stmts: &[Stmt::Return(StmtReturn { + range: TextRange::new(TextSize::new(0), TextSize::new(0)), + value: None, + })], + next: NextBlock::Terminate, + }; + + /// Return true if the block is a sentinel or fake block. + fn is_sentinel(&self) -> bool { + self.is_empty() || self.is_exception() + } + + /// Returns an empty block that terminates. + fn is_empty(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && self.stmts.is_empty() + } + + /// Returns true if `self` an [`BasicBlock::EXCEPTION`]. + fn is_exception(&self) -> bool { + matches!(self.next, NextBlock::Terminate) && BasicBlock::EXCEPTION.stmts == self.stmts + } +} + +/// Handle a loop block, such as a `while`, `for` or `async for` statement. +fn loop_block<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + condition: Condition<'stmt>, + body: &'stmt [Stmt], + orelse: &'stmt [Stmt], + after: Option, +) -> NextBlock<'stmt> { + let after_block = blocks.maybe_next_block_index(after, || orelse.is_empty()); + // NOTE: a while loop's body must not be empty, so we can safely + // create at least one block from it. + let last_statement_index = blocks.append_blocks(body, after); + let last_orelse_statement = blocks.append_blocks_if_not_empty(orelse, after_block); + // `create_blocks` always continues to the next block by + // default. However in a while loop we want to continue with the + // while block (we're about to create) to create the loop. + // NOTE: `blocks.len()` is an invalid index at time of creation + // as it points to the block which we're about to create. + blocks.change_next_block( + last_statement_index, + after_block, + blocks.blocks.next_index(), + |block| { + // For `break` statements we don't want to continue with the + // loop, but instead with the statement after the loop (i.e. + // not change anything). + !block.stmts.last().map_or(false, Stmt::is_break_stmt) + }, + ); + NextBlock::If { + condition, + next: last_statement_index, + orelse: last_orelse_statement, + } +} + +/// Handle a single match case. +/// +/// `next_after_block` is the block *after* the entire match statement that is +/// taken after this case is taken. +/// `orelse_after_block` is the next match case (or the block after the match +/// statement if this is the last case). +fn match_case<'stmt>( + blocks: &mut BasicBlocksBuilder<'stmt>, + match_stmt: &'stmt Stmt, + subject: &'stmt Expr, + case: &'stmt MatchCase, + next_after_block: BlockIndex, + orelse_after_block: BlockIndex, +) -> BasicBlock<'stmt> { + // FIXME: this is not ideal, we want to only use the `case` statement here, + // but that is type `MatchCase`, not `Stmt`. For now we'll point to the + // entire match statement. + let stmts = std::slice::from_ref(match_stmt); + let next_block_index = if case.body.is_empty() { + next_after_block + } else { + let from = blocks.last_index(); + let last_statement_index = blocks.append_blocks(&case.body, Some(next_after_block)); + if let Some(from) = from { + blocks.change_next_block(last_statement_index, from, next_after_block, |_| true); + } + last_statement_index + }; + // TODO: handle named arguments, e.g. + // ```python + // match $subject: + // case $binding: + // print($binding) + // ``` + // These should also return `NextBlock::Always`. + let next = if is_wildcard(case) { + // Wildcard case is always taken. + NextBlock::Always(next_block_index) + } else { + NextBlock::If { + condition: Condition::Match { subject, case }, + next: next_block_index, + orelse: orelse_after_block, + } + }; + BasicBlock { stmts, next } +} + +/// Returns true if `pattern` is a wildcard (`_`) pattern. +fn is_wildcard(pattern: &MatchCase) -> bool { + pattern.guard.is_none() + && matches!(&pattern.pattern, Pattern::MatchAs(PatternMatchAs { pattern, name, .. }) if pattern.is_none() && name.is_none()) +} + +#[derive(Debug, Default)] +struct BasicBlocksBuilder<'stmt> { + blocks: IndexVec>, +} + +impl<'stmt> BasicBlocksBuilder<'stmt> { + fn with_capacity(capacity: usize) -> Self { + Self { + blocks: IndexVec::with_capacity(capacity), + } + } + + /// Creates basic blocks from `stmts` and appends them to `blocks`. + fn create_blocks( + &mut self, + stmts: &'stmt [Stmt], + mut after: Option, + ) -> Option { + // We process the statements in reverse so that we can always point to the + // next block (as that should always be processed). + let mut stmts_iter = stmts.iter().enumerate().rev().peekable(); + while let Some((i, stmt)) = stmts_iter.next() { + let next = match stmt { + // Statements that continue to the next statement after execution. + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Break(_) + | Stmt::Pass(_) => self.unconditional_next_block(after), + Stmt::Continue(_) => { + // NOTE: the next branch gets fixed up in `change_next_block`. + self.unconditional_next_block(after) + } + // Statements that (can) divert the control flow. + Stmt::If(stmt) => { + let next_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.body)); + let orelse_after_block = + self.maybe_next_block_index(after, || needs_next_block(&stmt.orelse)); + let next = self.append_blocks_if_not_empty(&stmt.body, next_after_block); + let orelse = self.append_blocks_if_not_empty(&stmt.orelse, orelse_after_block); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::While(StmtWhile { + test: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Test(condition), body, orelse, after), + Stmt::For(StmtFor { + iter: condition, + body, + orelse, + .. + }) + | Stmt::AsyncFor(StmtAsyncFor { + iter: condition, + body, + orelse, + .. + }) => loop_block(self, Condition::Iterator(condition), body, orelse, after), + Stmt::Try(StmtTry { + body, + handlers, + orelse, + finalbody, + .. + }) + | Stmt::TryStar(StmtTryStar { + body, + handlers, + orelse, + finalbody, + .. + }) => { + // TODO: handle `try` statements. The `try` control flow is very + // complex, what blocks are and aren't taken and from which + // block the control flow is actually returns is **very** + // specific to the contents of the block. Read + // + // very carefully. + // For now we'll skip over it. + let _ = (body, handlers, orelse, finalbody); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::With(StmtWith { + items, + body, + type_comment, + .. + }) + | Stmt::AsyncWith(StmtAsyncWith { + items, + body, + type_comment, + .. + }) => { + // TODO: handle `with` statements, see + // . + // I recommend to `try` statements first as `with` can desugar + // to a `try` statement. + // For now we'll skip over it. + let _ = (items, body, type_comment); // Silence unused code warnings. + self.unconditional_next_block(after) + } + Stmt::Match(StmtMatch { subject, cases, .. }) => { + let next_after_block = self.maybe_next_block_index(after, || { + // We don't need need a next block if all cases don't need a + // next block, i.e. if no cases need a next block, and we + // have a wildcard case (to ensure one of the block is + // always taken). + // NOTE: match statement require at least one case, so we + // don't have to worry about empty `cases`. + // TODO: support exhaustive cases without a wildcard. + cases.iter().any(|case| needs_next_block(&case.body)) + || !cases.iter().any(is_wildcard) + }); + let mut orelse_after_block = next_after_block; + for case in cases.iter().rev() { + let block = match_case( + self, + stmt, + subject, + case, + next_after_block, + orelse_after_block, + ); + // For the case above this use the just added case as the + // `orelse` branch, this convert the match statement to + // (essentially) a bunch of if statements. + orelse_after_block = self.blocks.push(block); + } + // TODO: currently we don't include the lines before the match + // statement in the block, unlike what we do for other + // statements. + after = Some(orelse_after_block); + continue; + } + Stmt::Raise(_) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution, it's + // possible it's continued in an `catch` or `finally` block, + // possibly outside of the function. + // Also see `Stmt::Assert` handling. + NextBlock::Terminate + } + Stmt::Assert(stmt) => { + // TODO: this needs special handling within `try` and `with` + // statements. For now we just terminate the execution if the + // assertion fails, it's possible it's continued in an `catch` + // or `finally` block, possibly outside of the function. + // Also see `Stmt::Raise` handling. + let next = self.force_next_block_index(); + let orelse = self.fake_exception_block_index(); + NextBlock::If { + condition: Condition::Test(&stmt.test), + next, + orelse, + } + } + Stmt::Expr(stmt) => { + match &*stmt.value { + Expr::BoolOp(_) + | Expr::BinOp(_) + | Expr::UnaryOp(_) + | Expr::Dict(_) + | Expr::Set(_) + | Expr::Compare(_) + | Expr::Call(_) + | Expr::FormattedValue(_) + | Expr::JoinedStr(_) + | Expr::Constant(_) + | Expr::Attribute(_) + | Expr::Subscript(_) + | Expr::Starred(_) + | Expr::Name(_) + | Expr::List(_) + | Expr::Tuple(_) + | Expr::Slice(_) => self.unconditional_next_block(after), + // TODO: handle these expressions. + Expr::NamedExpr(_) + | Expr::Lambda(_) + | Expr::IfExp(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Await(_) + | Expr::Yield(_) + | Expr::YieldFrom(_) => self.unconditional_next_block(after), + } + } + // The tough branches are done, here is an easy one. + Stmt::Return(_) => NextBlock::Terminate, + }; + + // Include any statements in the block that don't divert the control flow. + let mut start = i; + let end = i + 1; + while stmts_iter + .next_if(|(_, stmt)| !is_control_flow_stmt(stmt)) + .is_some() + { + start -= 1; + } + + let block = BasicBlock { + stmts: &stmts[start..end], + next, + }; + after = Some(self.blocks.push(block)); + } + + after + } + + /// Calls [`create_blocks`] and returns this first block reached (i.e. the last + /// block). + fn append_blocks(&mut self, stmts: &'stmt [Stmt], after: Option) -> BlockIndex { + assert!(!stmts.is_empty()); + self.create_blocks(stmts, after) + .expect("Expect `create_blocks` to create a block if `stmts` is not empty") + } + + /// If `stmts` is not empty this calls [`create_blocks`] and returns this first + /// block reached (i.e. the last block). If `stmts` is empty this returns + /// `after` and doesn't change `blocks`. + fn append_blocks_if_not_empty( + &mut self, + stmts: &'stmt [Stmt], + after: BlockIndex, + ) -> BlockIndex { + if stmts.is_empty() { + after // Empty body, continue with block `after` it. + } else { + self.append_blocks(stmts, Some(after)) + } + } + + /// Select the next block from `blocks` unconditonally. + fn unconditional_next_block(&self, after: Option) -> NextBlock<'static> { + if let Some(after) = after { + return NextBlock::Always(after); + } + + // Either we continue with the next block (that is the last block `blocks`). + // Or it's the last statement, thus we terminate. + self.blocks + .last_index() + .map_or(NextBlock::Terminate, NextBlock::Always) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block. + fn force_next_block_index(&mut self) -> BlockIndex { + self.maybe_next_block_index(None, || true) + } + + /// Select the next block index from `blocks`. If there is no next block it will + /// add a fake/empty block if `condition` returns true. If `condition` returns + /// false the returned index may not be used. + fn maybe_next_block_index( + &mut self, + after: Option, + condition: impl FnOnce() -> bool, + ) -> BlockIndex { + if let Some(after) = after { + // Next block is already determined. + after + } else if let Some(idx) = self.blocks.last_index() { + // Otherwise we either continue with the next block (that is the last + // block in `blocks`). + idx + } else if condition() { + // Or if there are no blocks, but need one based on `condition` than we + // add a fake end block. + self.blocks.push(BasicBlock::EMPTY) + } else { + // NOTE: invalid, but because `condition` returned false this shouldn't + // be used. This only used as an optimisation to avoid adding fake end + // blocks. + BlockIndex::MAX + } + } + + /// Returns a block index for a fake exception block in `blocks`. + fn fake_exception_block_index(&mut self) -> BlockIndex { + for (i, block) in self.blocks.iter_enumerated() { + if block.is_exception() { + return i; + } + } + self.blocks.push(BasicBlock::EXCEPTION) + } + + /// Change the next basic block for the block, or chain of blocks, in index + /// `fixup_index` from `from` to `to`. + /// + /// This doesn't change the target if it's `NextBlock::Terminate`. + fn change_next_block( + &mut self, + mut fixup_index: BlockIndex, + from: BlockIndex, + to: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool + Copy, + ) { + /// Check if we found our target and if `check_condition` is met. + fn is_target( + block: &BasicBlock<'_>, + got: BlockIndex, + expected: BlockIndex, + check_condition: impl Fn(&BasicBlock) -> bool, + ) -> bool { + got == expected && check_condition(block) + } + + loop { + match self.blocks.get(fixup_index).map(|b| &b.next) { + Some(NextBlock::Always(next)) => { + let next = *next; + if is_target(&self.blocks[fixup_index], next, from, check_condition) { + // Found our target, change it. + self.blocks[fixup_index].next = NextBlock::Always(to); + } + return; + } + Some(NextBlock::If { + condition, + next, + orelse, + }) => { + let idx = fixup_index; + let condition = condition.clone(); + let next = *next; + let orelse = *orelse; + let new_next = if is_target(&self.blocks[idx], next, from, check_condition) { + // Found our target in the next branch, change it (below). + Some(to) + } else { + // Follow the chain. + fixup_index = next; + None + }; + + let new_orelse = if is_target(&self.blocks[idx], orelse, from, check_condition) + { + // Found our target in the else branch, change it (below). + Some(to) + } else if new_next.is_none() { + // If we done with the next branch we only continue with the + // else branch. + fixup_index = orelse; + None + } else { + // If we're not done with the next and else branches we need + // to deal with the else branch before deal with the next + // branch (in the next iteration). + self.change_next_block(orelse, from, to, check_condition); + None + }; + + let (next, orelse) = match (new_next, new_orelse) { + (Some(new_next), Some(new_orelse)) => (new_next, new_orelse), + (Some(new_next), None) => (new_next, orelse), + (None, Some(new_orelse)) => (next, new_orelse), + (None, None) => continue, // Not changing anything. + }; + + self.blocks[idx].next = NextBlock::If { + condition, + next, + orelse, + }; + } + Some(NextBlock::Terminate) | None => return, + } + } + } + + fn finish(mut self) -> BasicBlocks<'stmt> { + if self.blocks.is_empty() { + self.blocks.push(BasicBlock::EMPTY); + } + + BasicBlocks { + blocks: self.blocks, + } + } +} + +impl<'stmt> std::ops::Deref for BasicBlocksBuilder<'stmt> { + type Target = IndexSlice>; + + fn deref(&self) -> &Self::Target { + &self.blocks + } +} + +impl<'stmt> std::ops::DerefMut for BasicBlocksBuilder<'stmt> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.blocks + } +} + +/// Returns true if `stmts` need a next block, false otherwise. +fn needs_next_block(stmts: &[Stmt]) -> bool { + // No statements, we automatically continue with the next block. + let Some(last) = stmts.last() else { + return true; + }; + + match last { + Stmt::Return(_) | Stmt::Raise(_) => false, + Stmt::If(stmt) => needs_next_block(&stmt.body) || needs_next_block(&stmt.orelse), + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) + // TODO: check below. + | Stmt::Break(_) + | Stmt::Continue(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) => true, + } +} + +/// Returns true if `stmt` contains a control flow statement, e.g. an `if` or +/// `return` statement. +fn is_control_flow_stmt(stmt: &Stmt) -> bool { + match stmt { + Stmt::FunctionDef(_) + | Stmt::AsyncFunctionDef(_) + | Stmt::Import(_) + | Stmt::ImportFrom(_) + | Stmt::ClassDef(_) + | Stmt::Global(_) + | Stmt::Nonlocal(_) + | Stmt::Delete(_) + | Stmt::Assign(_) + | Stmt::AugAssign(_) + | Stmt::AnnAssign(_) + | Stmt::Expr(_) + | Stmt::Pass(_) => false, + Stmt::Return(_) + | Stmt::For(_) + | Stmt::AsyncFor(_) + | Stmt::While(_) + | Stmt::If(_) + | Stmt::With(_) + | Stmt::AsyncWith(_) + | Stmt::Match(_) + | Stmt::Raise(_) + | Stmt::Try(_) + | Stmt::TryStar(_) + | Stmt::Assert(_) + | Stmt::Break(_) + | Stmt::Continue(_) => true, + } +} + +/// Type to create a Mermaid graph. +/// +/// To learn amount Mermaid see , for the syntax +/// see . +struct MermaidGraph<'stmt, 'source> { + graph: &'stmt BasicBlocks<'stmt>, + source: &'source str, +} + +impl<'stmt, 'source> fmt::Display for MermaidGraph<'stmt, 'source> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Flowchart type of graph, top down. + writeln!(f, "flowchart TD")?; + + // List all blocks. + writeln!(f, " start((\"Start\"))")?; + writeln!(f, " return((\"End\"))")?; + for (i, block) in self.graph.blocks.iter().enumerate() { + let (open, close) = if block.is_sentinel() { + ("[[", "]]") + } else { + ("[", "]") + }; + write!(f, " block{i}{open}\"")?; + if block.is_empty() { + write!(f, "`*(empty)*`")?; + } else if block.is_exception() { + write!(f, "Exception raised")?; + } else { + for stmt in block.stmts { + let code_line = &self.source[stmt.range()].trim(); + mermaid_write_quoted_str(f, code_line)?; + write!(f, "\\n")?; + } + } + writeln!(f, "\"{close}")?; + } + writeln!(f)?; + + // Then link all the blocks. + writeln!(f, " start --> block{}", self.graph.blocks.len() - 1)?; + for (i, block) in self.graph.blocks.iter_enumerated().rev() { + let i = i.as_u32(); + match &block.next { + NextBlock::Always(target) => { + writeln!(f, " block{i} --> block{target}", target = target.as_u32())?; + } + NextBlock::If { + condition, + next, + orelse, + } => { + let condition_code = &self.source[condition.range()].trim(); + writeln!( + f, + " block{i} -- \"{condition_code}\" --> block{next}", + next = next.as_u32() + )?; + writeln!( + f, + " block{i} -- \"else\" --> block{orelse}", + orelse = orelse.as_u32() + )?; + } + NextBlock::Terminate => writeln!(f, " block{i} --> return")?, + } + } + + Ok(()) + } +} + +/// Escape double quotes (`"`) in `value` using `#quot;`. +fn mermaid_write_quoted_str(f: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result { + let mut parts = value.split('"'); + if let Some(v) = parts.next() { + write!(f, "{v}")?; + } + for v in parts { + write!(f, "#quot;{v}")?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use rustpython_parser::ast::Ranged; + use rustpython_parser::{parse, Mode}; + use std::fmt::Write; + use test_case::test_case; + + use crate::rules::ruff::rules::unreachable::{ + BasicBlocks, BlockIndex, MermaidGraph, NextBlock, + }; + + #[test_case("simple.py")] + #[test_case("if.py")] + #[test_case("while.py")] + #[test_case("for.py")] + #[test_case("async-for.py")] + //#[test_case("try.py")] // TODO. + #[test_case("raise.py")] + #[test_case("assert.py")] + #[test_case("match.py")] + fn control_flow_graph(filename: &str) { + let path = PathBuf::from_iter(["resources/test/fixtures/control-flow-graph", filename]); + let source = fs::read_to_string(&path).expect("failed to read file"); + let stmts = parse(&source, Mode::Module, filename) + .unwrap_or_else(|err| panic!("failed to parse source: '{source}': {err}")) + .expect_module() + .body; + + let mut output = String::new(); + + for (i, stmts) in stmts.into_iter().enumerate() { + let Some(func) = stmts.function_def_stmt() else { + use std::io::Write; + let _ = std::io::stderr().write_all(b"unexpected statement kind, ignoring"); + continue; + }; + + let got = BasicBlocks::from(&*func.body); + // Basic sanity checks. + assert!(!got.blocks.is_empty(), "basic blocks should never be empty"); + assert_eq!( + got.blocks.first().unwrap().next, + NextBlock::Terminate, + "first block should always terminate" + ); + + // All block index should be valid. + let valid = BlockIndex::from_usize(got.blocks.len()); + for block in &got.blocks { + match block.next { + NextBlock::Always(index) => assert!(index < valid, "invalid block index"), + NextBlock::If { next, orelse, .. } => { + assert!(next < valid, "invalid next block index"); + assert!(orelse < valid, "invalid orelse block index"); + } + NextBlock::Terminate => {} + } + } + + let got_mermaid = MermaidGraph { + graph: &got, + source: &source, + }; + + writeln!( + output, + "## Function {i}\n### Source\n```python\n{}\n```\n\n### Control Flow Graph\n```mermaid\n{}```\n", + &source[func.range()], + got_mermaid + ) + .unwrap(); + } + + insta::with_settings!({ + omit_expression => true, + input_file => filename, + description => "This is a Mermaid graph. You can use https://mermaid.live to visualize it as a diagram." + }, { + insta::assert_snapshot!(format!("{filename}.md"), output); + }); + } +} diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap new file mode 100644 index 0000000000..f2457017e3 --- /dev/null +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF014_RUF014.py.snap @@ -0,0 +1,249 @@ +--- +source: crates/ruff/src/rules/ruff/mod.rs +--- +RUF014.py:3:5: RUF014 Unreachable code in after_return + | +1 | def after_return(): +2 | return "reachable" +3 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +4 | +5 | async def also_works_on_async_functions(): + | + +RUF014.py:7:5: RUF014 Unreachable code in also_works_on_async_functions + | +5 | async def also_works_on_async_functions(): +6 | return "reachable" +7 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +8 | +9 | def if_always_true(): + | + +RUF014.py:12:5: RUF014 Unreachable code in if_always_true + | +10 | if True: +11 | return "reachable" +12 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +13 | +14 | def if_always_false(): + | + +RUF014.py:16:9: RUF014 Unreachable code in if_always_false + | +14 | def if_always_false(): +15 | if False: +16 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +17 | return "reachable" + | + +RUF014.py:21:9: RUF014 Unreachable code in if_elif_always_false + | +19 | def if_elif_always_false(): +20 | if False: +21 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +22 | elif False: +23 | return "also unreachable" + | + +RUF014.py:23:9: RUF014 Unreachable code in if_elif_always_false + | +21 | return "unreachable" +22 | elif False: +23 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +24 | return "reachable" + | + +RUF014.py:28:9: RUF014 Unreachable code in if_elif_always_true + | +26 | def if_elif_always_true(): +27 | if False: +28 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +29 | elif True: +30 | return "reachable" + | + +RUF014.py:31:5: RUF014 Unreachable code in if_elif_always_true + | +29 | elif True: +30 | return "reachable" +31 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +32 | +33 | def ends_with_if(): + | + +RUF014.py:35:9: RUF014 Unreachable code in ends_with_if + | +33 | def ends_with_if(): +34 | if False: +35 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +36 | else: +37 | return "reachable" + | + +RUF014.py:42:5: RUF014 Unreachable code in infinite_loop + | +40 | while True: +41 | continue +42 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +43 | +44 | ''' TODO: we could determine these, but we don't yet. + | + +RUF014.py:75:5: RUF014 Unreachable code in match_wildcard + | +73 | case _: +74 | return "reachable" +75 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +76 | +77 | def match_case_and_wildcard(status): + | + +RUF014.py:83:5: RUF014 Unreachable code in match_case_and_wildcard + | +81 | case _: +82 | return "reachable" +83 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +84 | +85 | def raise_exception(): + | + +RUF014.py:87:5: RUF014 Unreachable code in raise_exception + | +85 | def raise_exception(): +86 | raise Exception +87 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +88 | +89 | def while_false(): + | + +RUF014.py:91:9: RUF014 Unreachable code in while_false + | +89 | def while_false(): +90 | while False: +91 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +92 | return "reachable" + | + +RUF014.py:96:9: RUF014 Unreachable code in while_false_else + | +94 | def while_false_else(): +95 | while False: +96 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +97 | else: +98 | return "reachable" + | + +RUF014.py:102:9: RUF014 Unreachable code in while_false_else_return + | +100 | def while_false_else_return(): +101 | while False: +102 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +103 | else: +104 | return "reachable" + | + +RUF014.py:105:5: RUF014 Unreachable code in while_false_else_return + | +103 | else: +104 | return "reachable" +105 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +106 | +107 | def while_true(): + | + +RUF014.py:110:5: RUF014 Unreachable code in while_true + | +108 | while True: +109 | return "reachable" +110 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +111 | +112 | def while_true_else(): + | + +RUF014.py:116:9: RUF014 Unreachable code in while_true_else + | +114 | return "reachable" +115 | else: +116 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +117 | +118 | def while_true_else_return(): + | + +RUF014.py:122:9: RUF014 Unreachable code in while_true_else_return + | +120 | return "reachable" +121 | else: +122 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +123 | return "also unreachable" + | + +RUF014.py:123:5: RUF014 Unreachable code in while_true_else_return + | +121 | else: +122 | return "unreachable" +123 | return "also unreachable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^ RUF014 +124 | +125 | def while_false_var_i(): + | + +RUF014.py:128:9: RUF014 Unreachable code in while_false_var_i + | +126 | i = 0 +127 | while False: +128 | i += 1 + | ^^^^^^ RUF014 +129 | return i + | + +RUF014.py:135:5: RUF014 Unreachable code in while_true_var_i + | +133 | while True: +134 | i += 1 +135 | return i + | ^^^^^^^^ RUF014 +136 | +137 | def while_infinite(): + | + +RUF014.py:140:5: RUF014 Unreachable code in while_infinite + | +138 | while True: +139 | pass +140 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +141 | +142 | def while_if_true(): + | + +RUF014.py:146:5: RUF014 Unreachable code in while_if_true + | +144 | if True: +145 | return "reachable" +146 | return "unreachable" + | ^^^^^^^^^^^^^^^^^^^^ RUF014 +147 | +148 | # Test case found in the Bokeh repository that trigger a false positive. + | + + diff --git a/crates/ruff_dev/src/generate_json_schema.rs b/crates/ruff_dev/src/generate_json_schema.rs index bde0f4abdc..d284783d9f 100644 --- a/crates/ruff_dev/src/generate_json_schema.rs +++ b/crates/ruff_dev/src/generate_json_schema.rs @@ -61,7 +61,7 @@ mod tests { use super::{main, Args}; - #[test] + #[cfg_attr(not(feature = "unreachable-code"), test)] fn test_generate_json_schema() -> Result<()> { let mode = if env::var("RUFF_UPDATE_SCHEMA").as_deref() == Ok("1") { Mode::Write diff --git a/crates/ruff_index/src/slice.rs b/crates/ruff_index/src/slice.rs index 77401e7133..a6d3b033df 100644 --- a/crates/ruff_index/src/slice.rs +++ b/crates/ruff_index/src/slice.rs @@ -40,6 +40,11 @@ impl IndexSlice { } } + #[inline] + pub const fn first(&self) -> Option<&T> { + self.raw.first() + } + #[inline] pub const fn len(&self) -> usize { self.raw.len() @@ -63,6 +68,13 @@ impl IndexSlice { (0..self.len()).map(|n| I::new(n)) } + #[inline] + pub fn iter_enumerated( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator + '_ { + self.raw.iter().enumerate().map(|(n, t)| (I::new(n), t)) + } + #[inline] pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> { self.raw.iter_mut() diff --git a/crates/ruff_macros/src/newtype_index.rs b/crates/ruff_macros/src/newtype_index.rs index f6524b48a9..2c1f6e14ec 100644 --- a/crates/ruff_macros/src/newtype_index.rs +++ b/crates/ruff_macros/src/newtype_index.rs @@ -36,10 +36,11 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX as usize); + assert!(value <= Self::MAX_VALUE as usize); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. @@ -49,7 +50,7 @@ pub(super) fn generate_newtype_index(item: ItemStruct) -> syn::Result Self { - assert!(value <= Self::MAX); + assert!(value <= Self::MAX_VALUE); // SAFETY: // * The `value < u32::MAX` guarantees that the add doesn't overflow. From 521e6de2c8833f0229f3d51ee71fd70abd5805b8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 14:01:29 -0400 Subject: [PATCH 26/27] Fix eval detection for suspicious-eval-usage (#5506) Closes https://github.com/astral-sh/ruff/issues/5505. --- .../test/fixtures/flake8_bandit/S307.py | 12 ++++++++++ crates/ruff/src/checkers/ast/mod.rs | 4 +--- crates/ruff/src/rules/flake8_bandit/mod.rs | 1 + .../rules/flake8_bandit/rules/exec_used.rs | 22 ++++++++++++------- .../rules/suspicious_function_call.rs | 4 ++-- ...s__flake8_bandit__tests__S102_S102.py.snap | 4 ++-- ...s__flake8_bandit__tests__S307_S307.py.snap | 20 +++++++++++++++++ 7 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 crates/ruff/resources/test/fixtures/flake8_bandit/S307.py create mode 100644 crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py new file mode 100644 index 0000000000..06bccc084a --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_bandit/S307.py @@ -0,0 +1,12 @@ +import os + +print(eval("1+1")) # S307 +print(eval("os.getcwd()")) # S307 + + +class Class(object): + def eval(self): + print("hi") + + def foo(self): + self.eval() # OK diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index c4f3617120..67752031eb 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -2584,9 +2584,7 @@ where flake8_pie::rules::unnecessary_dict_kwargs(self, expr, keywords); } if self.enabled(Rule::ExecBuiltin) { - if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) { - self.diagnostics.push(diagnostic); - } + flake8_bandit::rules::exec_used(self, func); } if self.enabled(Rule::BadFilePermissions) { flake8_bandit::rules::bad_file_permissions(self, func, args, keywords); diff --git a/crates/ruff/src/rules/flake8_bandit/mod.rs b/crates/ruff/src/rules/flake8_bandit/mod.rs index 4abd69f58a..87d0e449eb 100644 --- a/crates/ruff/src/rules/flake8_bandit/mod.rs +++ b/crates/ruff/src/rules/flake8_bandit/mod.rs @@ -39,6 +39,7 @@ mod tests { #[test_case(Rule::SubprocessPopenWithShellEqualsTrue, Path::new("S602.py"))] #[test_case(Rule::SubprocessWithoutShellEqualsTrue, Path::new("S603.py"))] #[test_case(Rule::SuspiciousPickleUsage, Path::new("S301.py"))] + #[test_case(Rule::SuspiciousEvalUsage, Path::new("S307.py"))] #[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))] #[test_case(Rule::TryExceptContinue, Path::new("S112.py"))] #[test_case(Rule::TryExceptPass, Path::new("S110.py"))] diff --git a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs index 3ff3db8ded..d2dfb83fb5 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/exec_used.rs @@ -1,8 +1,10 @@ -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{Expr, Ranged}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; +use crate::checkers::ast::Checker; + #[violation] pub struct ExecBuiltin; @@ -14,12 +16,16 @@ impl Violation for ExecBuiltin { } /// S102 -pub(crate) fn exec_used(expr: &Expr, func: &Expr) -> Option { - let Expr::Name(ast::ExprName { id, .. }) = func else { - return None; - }; - if id != "exec" { - return None; +pub(crate) fn exec_used(checker: &mut Checker, func: &Expr) { + if checker + .semantic() + .resolve_call_path(func) + .map_or(false, |call_path| { + matches!(call_path.as_slice(), ["" | "builtin", "exec"]) + }) + { + checker + .diagnostics + .push(Diagnostic::new(ExecBuiltin, func.range())); } - Some(Diagnostic::new(ExecBuiltin, expr.range())) } diff --git a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs index cfbb0df50c..6a2add760d 100644 --- a/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -219,7 +219,7 @@ impl Violation for SuspiciousFTPLibUsage { } } -/// S001 +/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323 pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { let Expr::Call(ast::ExprCall { func, .. }) = expr else { return; @@ -246,7 +246,7 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, expr: &Expr) { // Mktemp ["tempfile", "mktemp"] => Some(SuspiciousMktempUsage.into()), // Eval - ["eval"] => Some(SuspiciousEvalUsage.into()), + ["" | "builtins", "eval"] => Some(SuspiciousEvalUsage.into()), // MarkSafe ["django", "utils", "safestring", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()), // URLOpen diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap index ccf9572377..075092ceda 100644 --- a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S102_S102.py.snap @@ -6,7 +6,7 @@ S102.py:3:5: S102 Use of `exec` detected 1 | def fn(): 2 | # Error 3 | exec('x = 2') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 4 | 5 | exec('y = 3') | @@ -16,7 +16,7 @@ S102.py:5:1: S102 Use of `exec` detected 3 | exec('x = 2') 4 | 5 | exec('y = 3') - | ^^^^^^^^^^^^^ S102 + | ^^^^ S102 | diff --git a/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap new file mode 100644 index 0000000000..f5c6ac82d8 --- /dev/null +++ b/crates/ruff/src/rules/flake8_bandit/snapshots/ruff__rules__flake8_bandit__tests__S307_S307.py.snap @@ -0,0 +1,20 @@ +--- +source: crates/ruff/src/rules/flake8_bandit/mod.rs +--- +S307.py:3:7: S307 Use of possibly insecure function; consider using `ast.literal_eval` + | +1 | import os +2 | +3 | print(eval("1+1")) # S307 + | ^^^^^^^^^^^ S307 +4 | print(eval("os.getcwd()")) # S307 + | + +S307.py:4:7: S307 Use of possibly insecure function; consider using `ast.literal_eval` + | +3 | print(eval("1+1")) # S307 +4 | print(eval("os.getcwd()")) # S307 + | ^^^^^^^^^^^^^^^^^^^ S307 + | + + From 75da72bd7fe786523f94064a2639096b61cb370f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 4 Jul 2023 14:06:01 -0400 Subject: [PATCH 27/27] Update documentation to list double-quote preference first (#5507) Closes https://github.com/astral-sh/ruff/issues/5496. --- .../rules/flake8_quotes/rules/from_tokens.rs | 20 +++++++++---------- .../ruff/src/rules/flake8_quotes/settings.rs | 4 ++-- ruff.schema.json | 14 ++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs index 6f0000e92c..710dec32cc 100644 --- a/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs +++ b/crates/ruff/src/rules/flake8_quotes/rules/from_tokens.rs @@ -42,16 +42,16 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString { fn message(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => format!("Double quotes found but single quotes preferred"), Quote::Double => format!("Single quotes found but double quotes preferred"), + Quote::Single => format!("Double quotes found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesInlineString { quote } = self; match quote { - Quote::Single => "Replace double quotes with single quotes".to_string(), Quote::Double => "Replace single quotes with double quotes".to_string(), + Quote::Single => "Replace double quotes with single quotes".to_string(), } } } @@ -91,16 +91,16 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString { fn message(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => format!("Double quote multiline found but single quotes preferred"), Quote::Double => format!("Single quote multiline found but double quotes preferred"), + Quote::Single => format!("Double quote multiline found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesMultilineString { quote } = self; match quote { - Quote::Single => "Replace double multiline quotes with single quotes".to_string(), Quote::Double => "Replace single multiline quotes with double quotes".to_string(), + Quote::Single => "Replace double multiline quotes with single quotes".to_string(), } } } @@ -139,16 +139,16 @@ impl AlwaysAutofixableViolation for BadQuotesDocstring { fn message(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => format!("Double quote docstring found but single quotes preferred"), Quote::Double => format!("Single quote docstring found but double quotes preferred"), + Quote::Single => format!("Double quote docstring found but single quotes preferred"), } } fn autofix_title(&self) -> String { let BadQuotesDocstring { quote } = self; match quote { - Quote::Single => "Replace double quotes docstring with single quotes".to_string(), Quote::Double => "Replace single quotes docstring with double quotes".to_string(), + Quote::Single => "Replace double quotes docstring with single quotes".to_string(), } } } @@ -186,8 +186,8 @@ impl AlwaysAutofixableViolation for AvoidableEscapedQuote { const fn good_single(quote: Quote) -> char { match quote { - Quote::Single => '\'', Quote::Double => '"', + Quote::Single => '\'', } } @@ -200,22 +200,22 @@ const fn bad_single(quote: Quote) -> char { const fn good_multiline(quote: Quote) -> &'static str { match quote { - Quote::Single => "'''", Quote::Double => "\"\"\"", + Quote::Single => "'''", } } const fn good_multiline_ending(quote: Quote) -> &'static str { match quote { - Quote::Single => "'\"\"\"", Quote::Double => "\"'''", + Quote::Single => "'\"\"\"", } } const fn good_docstring(quote: Quote) -> &'static str { match quote { - Quote::Single => "'", Quote::Double => "\"", + Quote::Single => "'", } } diff --git a/crates/ruff/src/rules/flake8_quotes/settings.rs b/crates/ruff/src/rules/flake8_quotes/settings.rs index 121501065e..d0f377a2fb 100644 --- a/crates/ruff/src/rules/flake8_quotes/settings.rs +++ b/crates/ruff/src/rules/flake8_quotes/settings.rs @@ -8,10 +8,10 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum Quote { - /// Use single quotes. - Single, /// Use double quotes. Double, + /// Use single quotes. + Single, } impl Default for Quote { diff --git a/ruff.schema.json b/ruff.schema.json index 785fac6520..064c0f7ec9 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1585,19 +1585,19 @@ }, "Quote": { "oneOf": [ - { - "description": "Use single quotes.", - "type": "string", - "enum": [ - "single" - ] - }, { "description": "Use double quotes.", "type": "string", "enum": [ "double" ] + }, + { + "description": "Use single quotes.", + "type": "string", + "enum": [ + "single" + ] } ] },